SpringSecurity OAuth2 自定义令牌配置(JW
目录:
- 自定义令牌配置
- 使用JWT替换默认令牌
- 扩展JWT
- JAVA中解析JWT
- 刷新令牌
Spring Security允许我们自定义令牌配置,比如不同的client_id对应不同的令牌,令牌的有效时间,令牌的存储策略等;我们也可以使用JWT来替换默认的令牌。
自定义令牌配置
我们让认证服务器AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter,并重写它的configure(ClientDetailsServiceConfigurer clients)方法:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenEnhancer tokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailService);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("test1")
.secret(new BCryptPasswordEncoder().encode("test1111"))
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(864000)
.scopes("all", "a", "b", "c")
.authorizedGrantTypes("password")
.and()
.withClient("test2")
.secret(new BCryptPasswordEncoder().encode("test2222"))
.accessTokenValiditySeconds(7200);
}
}
认证服务器在继承了AuthorizationServerConfigurerAdapter适配器后,需要重写configure(AuthorizationServerEndpointsConfigurer endpoints)方法,指定 AuthenticationManager和UserDetailService。
创建一个新的配置类SecurityConfig,在里面注册我们需要的AuthenticationManagerBean:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
此外,重写configure(ClientDetailsServiceConfigurer clients)方法主要配置了:
- 定义两个client_id,及客户端可以通过不同的client_id来获取不同的令牌;
- client_id为test1的令牌有效时间为3600秒,client_id为test2的令牌有效时间为7200秒;
- client_id为test1的refresh_token(下面会介绍到)有效时间为864000秒,即10天,也就是说在这10天内都可以通过refresh_token来换取新的令牌;
- 在获取client_id为test1的令牌的时候,scope只能指定为all,a,b或c中的某个值,否则将获取失败;
- 只能通过密码模式(password)来获取client_id为test1的令牌,而test2则无限制。
启动项目后使用密码模式获取test1的令牌:
image.png
和前面介绍的那样,头部需要传入test1:test1111经过base64加密后的值:
image.png
返回结果:
{
"access_token": "3f9864cc-dab5-420a-b129-560f409922d6",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all"
}
使用JWT替换默认令牌
使用JWT替换默认的令牌(默认令牌使用UUID生成)只需要指定TokenStore为JwtTokenStore即可。
创建一个JWTokenConfig配置类:
@Configuration
public class JWTokenConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("test_key"); // 签名密钥
return accessTokenConverter;
}
}
签名密钥为test_key。在配置类里配置好JwtTokenStore后,我们在认证服务器里指定它:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailService userDetailService;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.userDetailsService(userDetailService);
}
...
}
重启服务获取令牌,系统将返回如下格式令牌:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Njg3ODkzNTIsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2NzhlZTc5NC1lYzhhLTQxOTYtYmE2NS0xM2QwNjRiOGEyZWUiLCJjbGllbnRfaWQiOiJ0ZXN0MSIsInNjb3BlIjpbImFsbCJdfQ.ojuPewlJ_3hWRwOrlCwcHFZaNIX_78FQwj86Inw79_w",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all",
"jti": "678ee794-ec8a-4196-ba65-13d064b8a2ee"
}
将access_token
中的内容复制到https://jwt.io/网站解析下:
拓展JWT
上面的Token解析得到的PAYLOAD内容为:
{
"exp": 1568789352,
"user_name": "user",
"authorities": [
"admin"
],
"jti": "678ee794-ec8a-4196-ba65-13d064b8a2ee",
"client_id": "test1",
"scope": [
"all"
]
}
如果想在JWT中添加一些额外的信息,我们需要实现TokenEnhancer(Token增强器):
public class JWTTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> info = new HashMap<>();
info.put("message", "hello world");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
}
}
我们在Token中添加了message: hello world信息。然后在JWTokenConfig里注册该Bean:
@Configuration
public class JWTokenConfig {
......
@Bean
public TokenEnhancer tokenEnhancer() {
return new JWTokenEnhancer();
}
}
最后在认证服务器里配置该增强器:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailService userDetailService;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer tokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(tokenEnhancer);
enhancers.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancers);
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore)
.tokenEnhancer(enhancerChain)
.accessTokenConverter(jwtAccessTokenConverter)
.userDetailsService(userDetailService);
}
...
}
重启项目,再次获取令牌,系统返回:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc4OTkzNSwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiZDlmM2M4NGItMWNlNy00YWJmLWE1ZjUtODlkMTVmMTA2YTRhIiwiY2xpZW50X2lkIjoidGVzdDEifQ.WjIvH4dpaF8oKXh1qWuSP5o4slgpG9fAWzGTBUrwlA4",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all",
"message": "hello world",
"jti": "d9f3c84b-1ce7-4abf-a5f5-89d15f106a4a"
}
可以看到,在返回的JSON内容里已经多了我们添加的message信息,此外将access_token复制到jwt.io网站解析,内容如下:
{
"user_name": "user",
"scope": [
"all"
],
"exp": 1568789935,
"message": "hello world",
"authorities": [
"admin"
],
"jti": "d9f3c84b-1ce7-4abf-a5f5-89d15f106a4a",
"client_id": "test1"
}
解析后的JWT也包含了我们添加的message信息。
Java中解析JWT
要在Java代码中解析JWT,需要添加如下依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
编写接口index
@GetMapping("/index")
public Object index(HttpServletRequest request) {
String header = request.getHeader("Authorization");
String token = StringUtils.substringAfter(header, "bearer ");
return Jwts.parser().setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
}
signkey需要和JwtAccessTokenConverter中指定的签名密钥一致。重启项目,获取令牌后访问/index,输出内容如下:
{
"user_name": "user",
"scope": [
"all"
],
"exp": 1568790296,
"message": "hello world",
"authorities": [
"admin"
],
"jti": "4da297b1-9c12-4251-bf18-93bbc35d25bd",
"client_id": "test1"
}
刷新令牌
令牌过期后我们可以使用refresh_token来从系统中换取一个新的可用令牌。但是从前面的例子可以看到,在认证成功后返回的JSON信息里并没有包含refresh_token,要让系统返回refresh_token,需要在认证服务器自定义配置里添加如下配置:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
......
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("test1")
.secret(new BCryptPasswordEncoder().encode("test1111"))
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(864000)
.scopes("all", "a", "b", "c")
.and()
.withClient("test2")
.secret(new BCryptPasswordEncoder().encode("test2222"))
.accessTokenValiditySeconds(7200);
}
}
授权方式需要加上refresh_token,除了四种标准的OAuth2获取令牌方式外,Spring Security OAuth2内部把refresh_token当作一种拓展的获取令牌方式。
通过上面的配置,使用test1这个client_id获取令牌时将返回refresh_token,refresh_token的有效期为10天,即10天之内都可以用它换取新的可用令牌。
重启项目,认证成功后,系统返回如:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc5MDY4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMGMwZjg3N2ItMmNkYy00NmI2LWJkYTktNThhYmMzMmNkZDQ3IiwiY2xpZW50X2lkIjoidGVzdDEifQ.RmZExfQmbPgCo2UR8HChaTxRoUzkmKB2r2h7quSAUrw",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjBjMGY4NzdiLTJjZGMtNDZiNi1iZGE5LTU4YWJjMzJjZGQ0NyIsImV4cCI6MTU2OTY1MTA4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNDJmNTQyMjQtNDJiZS00ZWE2LTg5ODktOGE3ZTFhNjkzMjA5IiwiY2xpZW50X2lkIjoidGVzdDEifQ.1MLmQYoh--ExRuuf0_glHApPnTyCBi9UbZoZM3-76Ds",
"expires_in": 3599,
"scope": "all",
"message": "hello world",
"jti": "0c0f877b-2cdc-46b6-bda9-58abc32cdd47"
}
假设现在access_token过期了,我们用refresh_token去换取新的令牌。使用postman发送如下请求:
image.png
image.png
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU2ODc5MDgxMSwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMjdkOTNkMWQtNzczNi00ODQzLThjOTQtZWI1ZjdkMzIyMWJlIiwiY2xpZW50X2lkIjoidGVzdDEifQ.733DihtA3G3GkfFT82Bu-Z3FYuWHWxX8l_A0hON3XO8",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjI3ZDkzZDFkLTc3MzYtNDg0My04Yzk0LWViNWY3ZDMyMjFiZSIsImV4cCI6MTU2OTY1MTA4OCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNDJmNTQyMjQtNDJiZS00ZWE2LTg5ODktOGE3ZTFhNjkzMjA5IiwiY2xpZW50X2lkIjoidGVzdDEifQ.61o51OwW7twZ3tDRzEu__ho0bwwpIgwDPytkpnF00u8",
"expires_in": 3599,
"scope": "all",
"message": "hello world",
"jti": "27d93d1d-7736-4843-8c94-eb5f7d3221be"
}
源码地址:https://github.com/lbshold/springboot/tree/master/Spring-Security-OAuth2-JWT