如需转载,请根据 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 许可,附上本文作者及链接。
本文作者: 执笔成念
作者昵称: zbcn
本文链接: https://1363653611.github.io/zbcn.github.io/2021/01/15/springcloud15-oauth2%E7%BB%93%E5%90%88jwt/
Spring Cloud Security:Oauth2结合JWT使用
Spring Cloud Security 为构建安全的SpringBoot应用提供了一系列解决方案,结合Oauth2还可以实现更多功能,比如使用JWT令牌存储信息,刷新令牌功能,本文将对其结合JWT使用进行详细介绍。
JWT简介
JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。
JWT的组成
- JWT token的格式:header.payload.signature;
- header中用于存放签名的生成算法;
1 | { |
2 | "alg": "HS256", |
3 | "typ": "JWT" |
4 | } |
- payload中用于存放数据,比如过期时间、用户名、用户所拥有的权限等;
1 | { |
2 | "exp": 1572682831, |
3 | "user_name": "zbcn", |
4 | "authorities": [ |
5 | "admin" |
6 | ], |
7 | "jti": "c1a0645a-28b5-4468-b4c7-9623131853af", |
8 | "client_id": "admin", |
9 | "scope": [ |
10 | "all" |
11 | ] |
12 | } |
- signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败。
JWT实例
这是一个JWT的字符串:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzI2ODI4MzEsInVzZXJfbmFtZSI6InpiY24iLCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiJjMWEwNjQ1YS0yOGI1LTQ0NjgtYjRjNy05NjIzMTMxODUzYWYiLCJjbGllbnRfaWQiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdfQ.dlFafcwO7wOP9Y2Hw0aJYD4tVdS1GIx6EpqJD_ICe1I |
- 可以在该网站上获得解析结果:https://jwt.io/
创建oauth2-jwt-server模块
该模块只是对oauth2-server模块的扩展,直接复制过来扩展下下即可。
oauth2中存储令牌的方式
在入门学习时,我们都是把令牌存储在内存中的,这样如果部署多个服务,就会导致无法使用令牌的问题。 Spring Cloud Security中有两种存储令牌的方式可用于解决该问题,一种是使用Redis来存储,另一种是使用JWT来存储。
使用Redis存储令牌
- 在pom.xml中添加Redis相关依赖:
1 | <dependency> |
2 | <groupId>org.springframework.boot</groupId> |
3 | <artifactId>spring-boot-starter-data-redis</artifactId> |
4 | </dependency> |
- 在application.yml中添加redis相关配置
1 | spring: |
2 | redis: #redis相关配置 |
3 | password: 123456 #有密码时设置 |
- 添加在Redis中存储令牌的配置:
1 |
|
2 | public class RedisTokenStoreConfig { |
3 | |
4 | private RedisConnectionFactory redisConnectionFactory; |
5 | |
6 | |
7 | public TokenStore redisTokenStore (){ |
8 | return new RedisTokenStore(redisConnectionFactory); |
9 | } |
10 | } |
- 在认证服务器配置中指定令牌的存储策略为Redis:
1 | /** |
2 | * 认证服务器配置 |
3 | */ |
4 |
|
5 |
|
6 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { |
7 | |
8 | |
9 | private PasswordEncoder passwordEncoder; |
10 | |
11 | |
12 | private AuthenticationManager authenticationManager; |
13 | |
14 | |
15 | private UserService userService; |
16 | |
17 | |
18 | "redisTokenStore") ( |
19 | private TokenStore tokenStore; |
20 | |
21 | /** |
22 | * 使用密码模式需要配置 |
23 | */ |
24 | |
25 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { |
26 | endpoints.authenticationManager(authenticationManager) |
27 | .userDetailsService(userService) |
28 | .tokenStore(tokenStore);//配置令牌存储策略 |
29 | } |
30 | |
31 | //省略代码... |
32 | } |
- 运行项目后使用密码模式来获取令牌,访问如下地址:http://localhost:9401/oauth/token
- 进行获取令牌操作,可以发现令牌已经被存储到Redis中。
使用JWT存储令牌
- 添加使用JWT存储令牌的配置:
1 |
|
2 | public class JwtTokenStoreConfig { |
3 | |
4 | public TokenStore jwtTokenStore() { |
5 | return new JwtTokenStore(jwtAccessTokenConverter()); |
6 | } |
7 | |
8 | |
9 | public JwtAccessTokenConverter jwtAccessTokenConverter() { |
10 | JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); |
11 | accessTokenConverter.setSigningKey("test_key");//配置JWT使用的秘钥 |
12 | return accessTokenConverter; |
13 | } |
14 | } |
- 在认证服务器配置中指定令牌的存储策略为JWT:
1 | /** |
2 | * 认证服务器配置 |
3 | */ |
4 |
|
5 |
|
6 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { |
7 | |
8 | |
9 | private PasswordEncoder passwordEncoder; |
10 | |
11 | |
12 | private AuthenticationManager authenticationManager; |
13 | |
14 | |
15 | private UserService userService; |
16 | |
17 | |
18 | "jwtTokenStore") ( |
19 | private TokenStore tokenStore; |
20 | |
21 | private JwtAccessTokenConverter jwtAccessTokenConverter; |
22 | |
23 | /** |
24 | * 使用密码模式需要配置 |
25 | */ |
26 | |
27 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { |
28 | endpoints.authenticationManager(authenticationManager) |
29 | .userDetailsService(userService) |
30 | .tokenStore(tokenStore) //配置令牌存储策略 |
31 | .accessTokenConverter(jwtAccessTokenConverter); |
32 | } |
33 | |
34 | //省略代码... |
35 | } |
- 运行项目后使用密码模式来获取令牌,访问如下地址:http://localhost:9401/oauth/token
- 发现获取到的令牌已经变成了JWT令牌,将access_token拿到https://jwt.io/ 网站上去解析下可以获得其中内容。
1 | { |
2 | "exp": 1608086604, |
3 | "user_name": "zbcn", |
4 | "authorities": [ |
5 | "admin" |
6 | ], |
7 | "jti": "2da6377f-3d06-4830-a42e-e877a4009b5a", |
8 | "client_id": "admin", |
9 | "scope": [ |
10 | "all" |
11 | ] |
12 | } |
扩展JWT中存储的内容
有时候我们需要扩展JWT中存储的内容,这里我们在JWT中扩展一个key为enhance
,value为enhance info
的数据。
- 继承TokenEnhancer实现一个JWT内容增强器:
1 | public class JwtTokenEnhancer implements TokenEnhancer { |
2 | |
3 | public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { |
4 | Map<String, Object> info = new HashMap<>(); |
5 | info.put("enhance", "enhance info"); |
6 | ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info); |
7 | return oAuth2AccessToken; |
8 | } |
9 | } |
- 创建一个JwtTokenEnhancer实例:
1 | /** |
2 | * 使用Jwt存储token的配置 |
3 | */ |
4 |
|
5 | public class JwtTokenStoreConfig { |
6 | |
7 | //省略代码... |
8 | |
9 | |
10 | public JwtTokenEnhancer jwtTokenEnhancer() { |
11 | return new JwtTokenEnhancer(); |
12 | } |
13 | } |
- 在认证服务器配置中配置JWT的内容增强器:
1 | /** |
2 | * 认证服务器配置 |
3 | */ |
4 |
|
5 |
|
6 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { |
7 | |
8 | |
9 | private PasswordEncoder passwordEncoder; |
10 | |
11 | |
12 | private AuthenticationManager authenticationManager; |
13 | |
14 | |
15 | private UserService userService; |
16 | |
17 | |
18 | "jwtTokenStore") ( |
19 | private TokenStore tokenStore; |
20 | |
21 | private JwtAccessTokenConverter jwtAccessTokenConverter; |
22 | |
23 | private JwtTokenEnhancer jwtTokenEnhancer; |
24 | |
25 | /** |
26 | * 使用密码模式需要配置 |
27 | */ |
28 | |
29 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { |
30 | TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); |
31 | List<TokenEnhancer> delegates = new ArrayList<>(); |
32 | delegates.add(jwtTokenEnhancer); //配置JWT的内容增强器 |
33 | delegates.add(jwtAccessTokenConverter); |
34 | enhancerChain.setTokenEnhancers(delegates); |
35 | endpoints.authenticationManager(authenticationManager) |
36 | .userDetailsService(userService) |
37 | .tokenStore(tokenStore) //配置令牌存储策略 |
38 | .accessTokenConverter(jwtAccessTokenConverter) |
39 | .tokenEnhancer(enhancerChain); |
40 | } |
41 | |
42 | //省略代码... |
43 | } |
- 运行项目后使用密码模式来获取令牌,之后对令牌进行解析,发现已经包含扩展的内容。
1 | { |
2 | "user_name": "zbcn", |
3 | "scope": [ |
4 | "all" |
5 | ], |
6 | "exp": 1608087448, |
7 | "authorities": [ |
8 | "admin" |
9 | ], |
10 | "jti": "80c61410-09e4-448d-a2ee-4a3c243f96c4", |
11 | "client_id": "admin", |
12 | "enhance": "enhance info" |
13 | } |
Java中解析JWT中的内容
如果我们需要获取JWT中的信息,可以使用一个叫jjwt的工具包。
- 在pom.xml中添加相关依赖:
1 | <dependency> |
2 | <groupId>io.jsonwebtoken</groupId> |
3 | <artifactId>jjwt</artifactId> |
4 | <version>0.9.0</version> |
5 | </dependency> |
- 修改UserController类,使用jjwt工具类来解析Authorization头中存储的JWT内容。
1 | /** |
2 | * Created by macro on 2019/9/30. |
3 | */ |
4 |
|
5 | "/user") ( |
6 | public class UserController { |
7 | "/getCurrentUser") ( |
8 | public Object getCurrentUser(Authentication authentication, HttpServletRequest request) { |
9 | String header = request.getHeader("Authorization"); |
10 | String token = StrUtil.subAfter(header, "bearer ", false); |
11 | return Jwts.parser() |
12 | .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)) |
13 | .parseClaimsJws(token) |
14 | .getBody(); |
15 | } |
16 | |
17 | } |
- 在UserController中添加如下方法,使用jjwt工具类来解析Authorization头中存储的JWT内容。
1 | "/jwtCurrentUser") ( |
2 | public Object getJwtCurrentUser(Authentication authentication, HttpServletRequest request){ |
3 | String header = request.getHeader("Authorization"); |
4 | String token = StrUtil.subAfter(header, "bearer ", false); |
5 | return Jwts.parser() |
6 | .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)) |
7 | .parseClaimsJws(token) |
8 | .getBody(); |
9 | } |
- 将令牌放入
Authorization
头中,访问如下地址获取信息:http://localhost:9401/user/getCurrentUser
问题:401Unauthorized 踩坑:请求后返回:
访问始终返回如下信息,而且将 user 信息中的password 清空,
1 | { |
2 | "error": "invalid_token", |
3 | "error_description": "Invalid access token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ6YmNuIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYwODEwMDQ3MiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiZmUzMTY1ODctYTY2NC00MDNhLWIyZWUtZTYwZDAyZjIxZTc5IiwiY2xpZW50X2lkIjoiYWRtaW4iLCJlbmhhbmNlIjoiZW5oYW5jZSBpbmZvIn0.JdvnhhAtufGc7L5IUS2HNtMBLzJsdxpDGHYAmANLrPM" |
4 | } |
原因: TokenStore 冲突导致, 由于 配置为如下:
1 | //RedisTokenStoreConfig 中 |
2 |
|
3 |
|
4 | public TokenStore redisTokenStore (){ |
5 | return new RedisTokenStore(redisConnectionFactory); |
6 | } |
7 | //JwtTokenStoreConfig 中 |
8 |
|
9 | public JwtTokenEnhancer jwtTokenEnhancer() { |
10 | return new JwtTokenEnhancer(); |
11 | } |
导致 在程序运行时,将 redisTokenStore 默认使用了 redisTokenStore 。出现了问题。
在运行 DefaultTokenServices# loadAuthentication(String accessTokenValue) 方法时,默认使用了 redisTokenStore 。报错
解决方案:
- 将 redis 相关的令牌配置关闭。只使用 jwt的方式。
ResourceServerConfig
中指定使用哪个 tokenStore
1 |
|
2 |
|
3 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter { |
4 | |
5 | |
6 | "redisTokenStore") ( |
7 | private TokenStore tokenStore; |
8 | |
9 | |
10 | "jwtTokenStore") ( |
11 | private TokenStore jwtTokenStore; |
12 | |
13 | |
14 | public void configure(HttpSecurity http) throws Exception { |
15 | http.authorizeRequests() |
16 | .anyRequest() |
17 | .authenticated() |
18 | .and() |
19 | .requestMatchers() |
20 | .antMatchers("/user/**");//配置需要保护的资源路径 |
21 | } |
22 | |
23 | public void configure(ResourceServerSecurityConfigurer resources) throws Exception { |
24 | // 指定使用 哪个 tokenStore |
25 | resources.tokenStore(jwtTokenStore); |
26 | //resources.tokenStore(tokenStore); |
27 | } |
28 | } |
仔细观察日志,里面必有问题根源。*
刷新令牌
在Spring Cloud Security 中使用oauth2时,如果令牌失效了,可以使用刷新令牌通过refresh_token的授权模式再次获取access_token。
- 只需修改认证服务器的配置,添加refresh_token的授权模式即可。
1 | /** |
2 | * 认证服务器配置 |
3 | */ |
4 |
|
5 |
|
6 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { |
7 | |
8 | |
9 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { |
10 | clients.inMemory() |
11 | .withClient("admin") |
12 | .secret(passwordEncoder.encode("admin123456")) |
13 | .accessTokenValiditySeconds(3600) |
14 | .refreshTokenValiditySeconds(864000) |
15 | .redirectUris("http://www.baidu.com") |
16 | .autoApprove(true) //自动授权配置 |
17 | .scopes("all") |
18 | .authorizedGrantTypes("authorization_code","password","refresh_token"); //添加授权模式 |
19 | } |
20 | } |
使用刷新令牌模式来获取新的令牌,访问如下地址:http://localhost:9401/oauth/token
使用到的模块
1 | ZBCN-SERVER |
2 | └── zbcn-author/oauth2-jwt-server -- 使用jwt的oauth2认证测试服务 |