OAuth 2 有四种授权模式,分别是授权码模式(authorization code)、简化模式(implicit)、密码模式(resource owner password credentials)、客户端模式(client credentials),具体 OAuth2 是什么,可以参考这篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
本文我们将使用授权码模式和密码模式两种方式来实现用户认证和授权管理。
OAuth2 其实是一个关于授权的网络标准,它制定了设计思路和运行流程,利用这个标准我们其实是可以自己实现 OAuth2 的认证过程的。今天要介绍的 spring-cloud-starter-oauth2 ,其实是 Spring Cloud 按照 OAuth2 的标准并结合 spring-security 封装好的一个具体实现。
什么情况下需要用 OAuth2
首先大家最熟悉的就是几乎每个人都用过的,比如用微信登录、用 QQ 登录、用微博登录、用 Google 账号登录、用 github 授权登录等等,这些都是典型的 OAuth2 使用场景。假设我们做了一个自己的服务平台,如果不使用 OAuth2 登录方式,那么我们需要用户先完成注册,然后用注册号的账号密码或者用手机验证码登录。而使用了 OAuth2 之后,相信很多人使用过、甚至开发过公众号网页服务、小程序,当我们进入网页、小程序界面,第一次使用就无需注册,直接使用微信授权登录即可,大大提高了使用效率。因为每个人都有微信号,有了微信就可以马上使用第三方服务,这体验不要太好了。而对于我们的服务来说,我们也不需要存储用户的密码,只要存储认证平台返回的唯一ID 和用户信息即可。
以上是使用了 OAuth2 的授权码模式,利用第三方的权威平台实现用户身份的认证。当然了,如果你的公司内部有很多个服务,可以专门提取出一个认证中心,这个认证中心就充当上面所说的权威认证平台的角色,所有的服务都要到这个认证中心做认证。
这样一说,发现没,这其实就是个单点登录的功能。这就是另外一种使用场景,对于多服务的平台,可以使用 OAuth2 实现服务的单点登录,只做一次登录,就可以在多个服务中自由穿行,当然仅限于授权范围内的服务和接口。
实现统一认证功能
本篇先介绍密码模式实现的单点登录,下一篇再继续说授权码模式。
在微服务横行的今天,谁敢说自己手上没几个微服务。微服务减少了服务间的耦合,同时也在某些方面增加了系统的复杂度,比如说用户认证。假设我们这里实现了一个电商平台,用户看到的就是一个 APP 或者一个 web 站点,实际上背后是由多个独立的服务构成的,比如用户服务、订单服务、产品服务等。用户只要第一次输入用户名、密码完成登录后,一段时间内,都可以任意访问各个页面,比如产品列表页面、我的订单页面、我的关注等页面。
我们可以想象一下,自然能够想到,在请求各个服务、各个接口的时候,一定携带着什么凭证,然后各个服务才知道请求接口的用户是哪个,不然肯定有问题,那其实这里面的凭证简单来说就是一个 Token,标识用户身份的 Token。
系统架构说明
认证中心:oauth2-auth-server,OAuth2 主要实现端,Token 的生成、刷新、验证都在认证中心完成。
订单服务:oauth2-client-order-server,微服务之一,接收到请求后会到认证中心验证。
用户服务:oauth2-client-user-server,微服务之二,接收到请求后会到认证中心验证。
客户端:例如 APP 端、web 端 等终端
上图描述了使用了 OAuth2 的客户端与微服务间的请求过程。大致的过程就是客户端用用户名和密码到认证服务端换取 token,返回给客户端,客户端拿着 token 去各个微服务请求数据接口,一般这个 token 是放到 header 中的。当微服务接到请求后,先要拿着 token 去认证服务端检查 token 的合法性,如果合法,再根据用户所属的角色及具有的权限动态的返回数据。
创建并配置认证服务端
配置最多的就是认证服务端,验证账号、密码,存储 token,检查 token ,刷新 token 等都是认证服务端的工作。
1、引入需要的 maven 包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
spring-cloud-starter-oauth2包含了 spring-cloud-starter-security,所以不用再单独引入了。之所以引入 redis 包,是因为下面会介绍一种用 redis 存储 token 的方式。
2、配置好 application.yml
将项目基本配置设置好,并加入有关 redis 的配置,稍后会用到。
spring: application: name: auth-server redis: database: 2 host: localhost port: 32768 password: 1qaz@WSX jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 100ms server: port: 6001 management: endpoint: health: enabled: true
3、spring security 基础配置
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 允许匿名访问所有接口 主要是 oauth 接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**").permitAll(); } }
使用@EnableWebSecurity
注解修饰,并继承自WebSecurityConfigurerAdapter
类。
这个类的重点就是声明 PasswordEncoder
和 AuthenticationManager
两个 Bean。稍后会用到。其中 BCryptPasswordEncoder
是一个密码加密工具类,它可以实现不可逆的加密,AuthenticationManager
是为了实现 OAuth2 的 password 模式必须要指定的授权管理 Bean。
4、实现 UserDetailsService
如果你之前用过 Security 的话,那肯定对这个类很熟悉,它是实现用户身份验证的一种方式,也是最简单方便的一种。另外还有结合 AuthenticationProvider
的方式,有机会讲 Security 的时候再展开来讲吧。
UserDetailsService
的核心就是 loadUserByUsername
方法,它要接收一个字符串参数,也就是传过来的用户名,返回一个 UserDetails
对象。
@Slf4j @Component(value = "kiteUserDetailsService") public class KiteUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { log.info("usernameis:" + username); // 查询数据库操作 if(!username.equals("admin")){ throw new UsernameNotFoundException("the user is not found"); }else{ // 用户角色也应在数据库中获取 String role = "ROLE_ADMIN"; List<SimpleGrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(role)); // 线上环境应该通过用户名查询数据库获取加密后的密码 String password = passwordEncoder.encode("123456"); return new org.springframework.security.core.userdetails.User(username,password, authorities); } } }
这里为了做演示,把用户名、密码和所属角色都写在代码里了,正式环境中,这里应该是从 数据库或者其他地方根据用户名将加密后的密码及所属角色查出来的。账号 admin ,密码 123456,稍后在换取 token 的时候会用到。并且给这个用户设置 "ROLE_ADMIN" 角色。
5、OAuth2 配置文件
创建一个配置文件继承自 AuthorizationServerConfigurerAdapter
.
@Configuration @EnableAuthorizationServer public class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired public PasswordEncoder passwordEncoder; @Autowired public UserDetailsService kiteUserDetailsService; @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore redisTokenStore; @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * redis token 方式 */ endpoints.authenticationManager(authenticationManager) .userDetailsService(kiteUserDetailsService) .tokenStore(redisTokenStore); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("order-client") .secret(passwordEncoder.encode("order-secret-8888")) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .accessTokenValiditySeconds(3600) .scopes("all") .and() .withClient("user-client") .secret(passwordEncoder.encode("user-secret-8888")) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .accessTokenValiditySeconds(3600) .scopes("all"); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); security.checkTokenAccess("isAuthenticated()"); security.tokenKeyAccess("isAuthenticated()"); } }
有三个 configure 方法的重写。
AuthorizationServerEndpointsConfigurer
参数的重写
endpoints.authenticationManager(authenticationManager) .userDetailsService(kiteUserDetailsService) .tokenStore(redisTokenStore);
authenticationManage()
调用此方法才能支持 password 模式。
userDetailsService()
设置用户验证服务。
tokenStore()
指定 token 的存储方式。
redisTokenStore Bean 的定义如下:
@Configuration public class RedisTokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ return new RedisTokenStore(redisConnectionFactory); } }
ClientDetailsServiceConfigurer
参数的重写,在这里定义各个端的约束条件。包括
ClientId、Client-Secret:这两个参数对应请求端定义的 cleint-id 和 client-secret
authorizedGrantTypes 可以包括如下几种设置中的一种或多种:
- authorization_code:授权码类型。
- implicit:隐式授权类型。
- password:资源所有者(即用户)密码类型。
- client_credentials:客户端凭据(客户端ID以及Key)类型。
- refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。
accessTokenValiditySeconds:token 的有效期
scopes:用来限制客户端访问的权限,在换取的 token 的时候会带上 scope 参数,只有在 scopes 定义内的,才可以正常换取 token。
上面代码中是使用 inMemory 方式存储的,将配置保存到内存中,相当于硬编码了。正式环境下的做法是持久化到数据库中,比如 mysql 中。
具体的做法如下:
- 在数据库中增加表,并插入数据
create table oauth_client_details ( client_id VARCHAR(256) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(256) ); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
注意: client_secret 字段不能直接是 secret 的原始值,需要经过加密。因为是用的 BCryptPasswordEncoder
,所以最终插入的值应该是经过 BCryptPasswordEncoder.encode()
之后的值。
- 然后在配置文件 application.yml 中添加关于数据库的配置
spring: datasource: url: jdbc:mysql://localhost:3306/spring_cloud?characterEncoding=UTF-8&useSSL=false username: root password: password hikari: connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 maximum-pool-size: 9
Spring Boot 2.0 之后默认使用 hikari 作为数据库连接池。如果使用其他连接池需要引入相关包,然后对应的增加配置。
- 在 OAuth2 配置类(OAuth2Config)中增加 DataSource 的注入
@Autowired private DataSource dataSource;
- 将
public void configure(ClientDetailsServiceConfigurer clients)
重写方法修改为如下:
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource); jcsb.passwordEncoder(passwordEncoder); }
还有一个重写的方法 public void configure(AuthorizationServerSecurityConfigurer security)
,这个方法限制客户端访问认证接口的权限。
security.allowFormAuthenticationForClients(); security.checkTokenAccess("isAuthenticated()"); security.tokenKeyAccess("isAuthenticated()");
第一行代码是允许客户端访问 OAuth2 授权接口,否则请求 token 会返回 401。
第二行和第三行分别是允许已授权用户访问 checkToken 接口和获取 token 接口。
完成之后,启动项目,如果你用的是 IDEA 会在下方的 Mapping 窗口中看到 oauth2 相关的 RESTful 接口。
主要有如下几个:
POST /oauth/authorize 授权码模式认证授权接口 GET/POST /oauth/token 获取 token 的接口 POST /oauth/check_token 检查 token 合法性接口
创建用户客户端项目
上面创建完成了认证服务端,下面开始创建一个客户端,对应到我们系统中的业务相关的微服务。我们假设这个微服务项目是管理用户相关数据的,所以叫做用户客户端。
1、引用相关的 maven 包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2、application.yml 配置文件
spring: application: name: client-user redis: database: 2 host: localhost port: 32768 password: 1qaz@WSX jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 100ms server: port: 6101 servlet: context-path: /client-user security: oauth2: client: client-id: user-client client-secret: user-secret-8888 user-authorization-uri: http://localhost:6001/oauth/authorize access-token-uri: http://localhost:6001/oauth/token resource: id: user-client user-info-uri: user-info authorization: check-token-access: http://localhost:6001/oauth/check_token
上面是常规配置信息以及 redis 配置,重点是下面的 security 的配置,这里的配置稍有不注意就会出现 401 或者其他问题。
client-id、client-secret 要和认证服务中的配置一致,如果是使用 inMemory 还是 jdbc 方式。
user-authorization-uri 是授权码认证方式需要的,下一篇文章再说。
access-token-uri 是密码模式需要用到的获取 token 的接口。
authorization.check-token-access 也是关键信息,当此服务端接收到来自客户端端的请求后,需要拿着请求中的 token 到认证服务端做 token 验证,就是请求的这个接口
Spring Cloud OAuth2 实现用户认证及单点登录(2)https://developer.aliyun.com/article/1551830