授权服务器是一个系统中比较重要的模块,主要提供请求的访问认证和资源的授权。在这里,使用 SpringSecurity + Oauth2 + Jwt 实现一个授权服务器。
背景
Spring Security OAuth 项目提供了基于 Oauth2 的授权服务器 (Authorization Server),Springboot 和 Springcloud 对其进行了封装,可以很方便引入。目前 Spring Security OAuth 项目已经被弃用,相关的功能会迁移到 Spring Security 5 中,不过并没有包含授权服务器,这意味着使用最新的 Spring Security 5 版本将不能使用授权服务器。官方建议使用第三方的授权服务器,不过鉴于开发者的强烈反馈。Spring 官方发起了 Spring Authorization Server 项目,该项目是由 Spring Security 主导的一个社区驱动项目,旨在向 Spring 社区提供授权服务器支持,目前的版本是 V 0.2.2,还没有达到 GA 的标准。
考虑到 Spring Authorization Server 项目还没有稳定,在这里仍然使用 Spring Security OAuth 项目。 Springboot 2.3.4 版本后 Spring Security 5.2.x. 不支持 Authorization Server , 需要选取之前的版本。
上面进到可以通过 Springboot 和 Springcloud 两种方式引入授权服务器 (Authorization Server),其中 Springboot 引入的包如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
Springcloud 包:1
2
3
4
5
6
7
8
9<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
只要选择其中一种即可,在这里使用的 Springcloud 的方式,版本如下:1
2<spring-boot.version>2.2.2.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
注意: Springboot 2.3.4 版本后 Spring Security 5.2.x. 不支持 Authorization Server
授权服务器
整体流程
配置 Spring Security OAuth 授权服务器,需要对 Oauth2 协议有所了解,如果不了解可以参考这两篇文章: OAuth 2.0 协议, 理解 OAuth 2.0.
配置授权服务器包含以下步骤:
- 初始化数据库表,包括存储客户端、token 及用户等信息的表;
- 配置用户读取及认证的方式;
- 配置客户端及 token 存储及读取方式;
- 配置 JWT;
- 配置授权相关的 endpoint.
初始化数据库表
在这里需要存储两类数据库表,一类是 Oauth2 相关的表,包括客户端及 token 表,这个可以由 Spring 官方提供。另一类是业务相关的用户权限的表,它们的表结构如下:
- Oauth2 表
1 | -- ---------------------------- |
为了测试,加入了两个客户端 dev 和 oauth2,dev 用于接口访问,oauth2 用于资源服务器。
1 | -- ---------------------------- |
- 用户权限表
1 | -- ---------------------------- |
为了简单起见,只创建了一个用户表,并添加了两个用户,密码为 123456.
配置用户认证信息
在 Spring security 中提供了通用的认证功能,需要指定三个信息,分别是:
- UserDetailsService: 用户查询接口;
- UserDetails: 用户详情;
- PasswordEncoder: 用于密码字段的加解密。
UserDetailsService
定义了如何获取用户信息,它是一个接口,定义了一个根据用户名称获取用户的方法,定义如下:1
2
3
4public interface UserDetailsService {
// 根据用户名返回用户信息
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
UserDetails
定义了一个用户具备什么元素,它也是一个接口,其定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public interface UserDetails extends Serializable {
// 返回用户具有的权限
Collection<? extends GrantedAuthority> getAuthorities();
// 获取用户密码
String getPassword();
// 获取用户名称
String getUsername();
// 用户是否已经过期
boolean isAccountNonExpired();
// 用户是否被锁定
boolean isAccountNonLocked();
// 密码是否过期
boolean isCredentialsNonExpired();
// 用户是否有效
boolean isEnabled();
}
PasswordEncoder
用于用户密码字段的加密及验证,其定义如下:1
2
3
4
5
6
7
8
9
10
11public interface PasswordEncoder {
// 密码加密
String encode(CharSequence var1);
// 密码匹配验证
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
WebSecurityConfigurerAdapter 类是 Spring security 中提供给用户进行安全配置的类,用户验证的功能也是在这个类进行配置。我们只需要继承该类加入我们的自定义配置即可,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 1. 开启 WebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 2. 自定义 serDetailsService 类
private MyUserDetailsService userService;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 3. 注册 serDetailsService 类
auth.userDetailsService(userService)
// 4. 注册 PasswordEncoder类
.passwordEncoder(new PasswordEncoder() {
// 对密码进行加密
public String encode(CharSequence charSequence) {
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
// 对密码进行判断匹配
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals(encode);
return res;
}
});
}
/**
* 返回 AuthenticationManager 类,在 Oauth2 中会复用该类进行用户的认证。
* @return AuthenticationManager
* @throws Exception 异常
*/
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManager();
}
/**
* 用于 Http Basic 认证方式中,用户名/密码的验证
* 在 oauth2 中 clientId/secret 的验证,此时不用密码不用加密
* @return PasswordEncoder
*/
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
public boolean matches(CharSequence charSequence, String s) {
return Objects.equals(charSequence.toString(), s);
}
};
}
}
配置客户端及 Token 相关信息
授权服务器相关的配置在 AuthorizationServerConfigurerAdapter 中配置,整体框架如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 1. 开启 AuthorizationServer 配置
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
/**
* 2. 注入权限验证控制器 来支持 password grant type
*/
private AuthenticationManager authenticationManager;
/**
* 3. 注入userDetailsService,开启refresh_token需要用到
*/
private MyUserDetailsService userDetailsService;
/**
* 4. 注入 tokenStore
* 保存 token的方式,一共有五种,分别是:
* 1. Memory
* 2. Jwt
* 3. Jwk
* 4. jdbc
* 5. redis
*/
private TokenStore tokenStore;
// 5. 配置 tokenStore
public TokenStore tokenStore() {
}
// 6. 配置接口的安全设置,如访问权限等等
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
// 7. 配置客户端访问服务
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
// 8. 配置 endpoint
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
客户端配置
客户端的相关设置是在 ClientDetailsServiceConfigurer 类进行的,在这里,将 Client 信息存储到 Mysql 中,其配置如下:1
2
3
4
5
6
7
8
9
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 1. 自定义 ClientDetailsService,可以加入缓存等技术
CustomClientDetailsService clientDetailsService = new CustomClientDetailsService(dataSource);
clients.withClientDetails(clientDetailsService);
// 2. 使用自带的 JdbcClientDetailsService
// clients.jdbc(dataSource);
}
JdbcClientDetailsService 类封装了访问 oauth_client_details 表的 sql 方法,默认是直接访问数据库,可以根据需要,自定义 JdbcClientDetailsService 类,扩展功能,如加入缓存。
Tokenstore配置
TokenStore 封装了存取 token 的方式,它有五种存储方式,分别是:1) Memory; 2) Jwt; 2) Jwk; 4) jdbc (db); 5) Redis。可以根据需要,自行选择,在这里我们使用 Redis 的存储方式,需要额外引入相关的 Jar 包。配置方式如下:
1 | public TokenStore tokenStore() { |
说明:token 使用 JWT token, tokenstore 也可以选择 redis, 不代表使用 Jwt, tokenstore 就一定要使用 Jwt 模式。
配置 JWT
授权服务器在默认情况下生成的 token 是类似一个 uuid 的随机字串,它本身不携带任何用户信息,资源服务器验证 token 的有效性都需要通过授权服务器进行,会加大网络开销。Jwt 则不同,它本身可以携带不敏感的业务信息,如用户名及权限消息,只需要向授权服务器获取一次签名字段的密钥(对称密钥或非对称密钥),在客户端(或资源服务器)就可以在本地验证 token 的有效性,有效提高系统的效率。
Jwt 的格式如下:
- Header: JSON 对象,用来描述 JWT 的元数据,alg 属性表示签名的算法,typ 标识 token 的类型;
- Payload: JSON 对象,重要部分,除了默认的字段,还可以扩展自定义字段,比如用户 ID、姓名、角色等等;
- Signature: 对 Header、Payload 这两部分进行签名,授权服务器使用私钥签名,然后在资源服务器使用公钥验签,保证数据不被篡改。
配置的流程包括:
- 设置 JwtAccessTokenConverter, 加入对 Jwt 的支持;
- 设置 Jwt 签名密钥;
- 设置 TokenEnhancer, 可以加入额外的业务信息;
- 将 1),4) 加入到授权服务器中。
设置 JwtAccessTokenConverter, 将设置签名密钥1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 使用非对称加密算法对token签名
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
/**
* 从classpath下的密钥库中获取密钥对(公钥+私钥)
*/
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource("oauth2.jks"), "123456".toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("oauth2");
return keyPair;
}
Jwt 签名有对称和非对称两种方式:
- 对称方式:授权服务器和资源服务器使用同一个密钥进行加签和验签 ,默认算法 HMAC;
- 非对称方式:授权服务器使用私钥加签,资源服务器使用公钥验签,默认算法 RSA;
项目中使用 RSA 非对称签名方式,具体实现步骤如下:
- 从密钥库获取密钥对(密钥 + 私钥),如 oauth2.jks;
- 授权服务器使用私钥对 token 签名;
- 授权服务器提供 /oauth/token_key 接口,资源服务器通过该接口获取公钥,验证 token 签名。
密钥库可以通过下面的命令生成:1
keytool -genkey -alias oauth2 -keyalg RSA -keystore oauth2.jks -storepass 123456
参数说明:
- -genkey 生成密钥
- -alias 别名
- -keyalg 密钥算法
- -keypass 密钥口令
- -keystore 生成密钥库的存储路径和名称
- -storepass 密钥库口令
设置 TokenEnhancer
1 | /** |
可以向 Jwt 中加入 userId 等业务字段。
将配置加入到授权服务器
1 |
|
通过 AuthorizationServerEndpointsConfigurer 类加入 Jwt 支持。
配置 endpoint
授权服务器提供了如下 endpoint :
- /oauth/authorize:授权端点
- /oauth/token:获取令牌端点
- /oauth/confirm_access:用户确认授权端点
- /oauth/check_token:校验令牌端点
- /oauth/error:用于在授权服务器中呈现错误
- /oauth/token_key:获取 jwt 公钥端点
为了让资源服务器可以访问这些 endpoint, 需要开通访问权限。
1 |
|
最后,Jwt, tokenstore, authenticationManager 及 userDetailsService 加入到 AuthorizationServerEndpointsConfigurer 中,完成对授权服务器的配置
1 |
|
资源服务器
配置资源服务器相对比较简单,包含如下流程。
引入依赖包
1 | <dependency> |
配置 ResourceServer
继承 ResourceServerConfigurerAdapter 类,并进行相关的配置
1 | 4j |
配置访问 URL
设置授权服务器 URL,如 check_token 及 token_key,同时可以设置公钥信息。
1 | security: |
开发资源接口
接下来我们开发需要受保护的 RESTFul 接口。
1 |
|
测试
获取 Jwt token
1 | $ curl -X POST -d "username=admin&password=123456&grant_type=password&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token | json_pp |
访问资源服务器
带上 access_token 访问资源服务器。
1 | $ curl http://localhost:9091/hello?name=world \ |
代码库:https://github.com/noahsarkzhang-ts/springboot-lab
参考:
3. 微服务中使用Spring Security + OAuth 2.0 + JWT 搭建认证授权服务
4. Spring Boot Security 整合 OAuth2 设计安全API接口服务
5. Spring Cloud实战 | 第六篇:Spring Cloud Gateway + Spring Security OAuth2 + JWT实现微服务统一认证授权鉴权