在前面幾節中,我們獲取到的令牌都是基於Spring Security OAuth2默認配置生成的,Spring Security允許我們自定義令牌配置,比如不同的client_id對應不同的令牌,令牌的有效時間,令牌的存儲策略等;我們也可以使用JWT來替換默認的令牌。
自定義令牌配置
我們讓認證服務器AuthorizationServerConfig繼承AuthorizationServerConfigurerAdapter,並重寫它的configure(ClientDetailsServiceConfigurer clients)方法:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
......
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailService userDetailService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailService);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("test1")
.secret("test1111")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(864000)
.scopes("all", "a", "b", "c")
.authorizedGrantTypes("password")
.and()
.withClient("test2")
.secret("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的令牌:
和前面介紹的那樣,頭部需要傳入test1:test1111經過base64加密后的值:
點擊發送后,意外的返回了錯誤!
控制台輸出了 Encoded password does not look like BCrypt 的告警。
查閱資料后發現,在新版本的spring-cloud-starter-oauth2指定client_secret的時候需要進行加密處理:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
......
@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);
}
}
在前面自定義登錄認證獲取令牌一節中,我們在MyAuthenticationSucessHandler判斷了client_secret的值是否正確。由於我們這里client_secret加密了,所以判斷邏輯需要調整為下面這樣:
...
else if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
throw new UnapprovedClientAuthenticationException("clientSecret不正確");
}
...
言歸正傳,修改后重啟項目,重新使用密碼模式獲取令牌:
{
"access_token": "c23376b0-efa3-4905-8356-8c9583c2a2a0",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all"
}
可以看到expires_in的時間是我們定義的3600秒。
將scope指定為d看看會有什么結果:
由於我們定義了只能通過密碼模式來獲取client_id為test1的令牌,所以我們看看將grant_type改為xxoo會有什么結果:
默認令牌是存儲在內存中的,我們可以將它保存到第三方存儲中,比如Redis。
創建TokenStoreConfig:
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore (){
return new RedisTokenStore(redisConnectionFactory);
}
}
然后在認證服務器里指定該令牌存儲策略。重寫configure(AuthorizationServerEndpointsConfigurer endpoints)方法:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore redisTokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(redisTokenStore);
}
......
}
重啟項目獲取令牌后,查看Redis中是否存儲了令牌相關信息:
可以看到,令牌信息已經存儲到Redis里了。
使用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 TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter);
}
......
}
重啟服務獲取令牌,系統將返回如下格式令牌:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjE1MzI1MDEsInVzZXJfbmFtZSI6Im1yYmlyZCIsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjJkZjY4MGNhLWFmN2QtNGU4Ni05OTdhLWI1ZmVkYzQxZmYwZSIsImNsaWVudF9pZCI6InRlc3QxIiwic2NvcGUiOltdfQ.dZ4SeuU3VWnSJKy5vELGQ0YkVRddcEydUlJAVovlycg",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all",
"jti": "2df680ca-af7d-4e86-997a-b5fedc41ff0e"
}
將access_token中的內容復制到https://jwt.io/網站解析下:
使用這個token訪問/index可以成功獲取到信息,這里就不演示了。
拓展JWT
上面的Token解析得到的PAYLOAD內容為:
{
"exp": 1561532501,
"user_name": "mrbird",
"authorities": [
"admin"
],
"jti": "2df680ca-af7d-4e86-997a-b5fedc41ff0e",
"client_id": "test1",
"scope": ["all"]
}
如果想在JWT中添加一些額外的信息,我們需要實現TokenEnhancer(Token增強器):
public class JWTokenEnhancer 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 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.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain);
}
......
}
重啟項目,再次獲取令牌,系統返回:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJtcmJpcmQiLCJzY29wZSI6W10sImV4cCI6MTU2MTUzNDQ1MCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiY2E1NDA3ZTEtMzBmZS00MzA3LThiYmItYjU2NGY5Y2ViOWUzIiwiY2xpZW50X2lkIjoidGVzdDEifQ.qW92ssifRKi_rxX2XIH2u4D5IUPVcKECv812hTpuUuA",
"token_type": "bearer",
"expires_in": 3599,
"message": "hello world",
"jti": "ca5407e1-30fe-4307-8bbb-b564f9ceb9e3"
}
可以看到,在返回的JSON內容里已經多了我們添加的message信息,此外將access_token復制到jwt.io網站解析,內容如下:
{
"user_name": "mrbird",
"scope": [],
"exp": 1561534450,
"message": "hello world",
"authorities": [
"admin"
],
"jti": "ca5407e1-30fe-4307-8bbb-b564f9ceb9e3",
"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(@AuthenticationPrincipal Authentication authentication, 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,輸出內容如下:
{
"exp": 1561557893,
"user_name": "mrbird",
"authorities": [
"admin"
],
"jti": "3c29f89a-1344-40d8-bcfd-1b9c45fb8b89",
"client_id": "test1",
"scope": [
"all"
]
}
刷新令牌
令牌過期后我們可以使用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.eyJleHAiOjE1NjE1NTgwOTcsInVzZXJfbmFtZSI6Im1yYmlyZCIsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6Ijg2NTdhMDBlLTFiM2MtNDA5NS1iMjNmLTJlMjUxOWExZmUwMiIsImNsaWVudF9pZCI6InRlc3QxIiwic2NvcGUiOlsiYWxsIl19.hrxKOz3NKY6Eq8k5QeOqKhXUQ4aAbicrb6J5y-LBRA0",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJtcmJpcmQiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiODY1N2EwMGUtMWIzYy00MDk1LWIyM2YtMmUyNTE5YTFmZTAyIiwiZXhwIjoxNTYyNDE4NDk3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2MTNjMDVlNS1hNzUzLTRmM2UtOWViOC1hZGE4MTJmY2IyYWQiLCJjbGllbnRfaWQiOiJ0ZXN0MSJ9.efw9OePFUN9X6UGMF3h9BF_KO3zqyIfpvfmE8XklBDs",
"expires_in": 3599,
"scope": "all",
"jti": "8657a00e-1b3c-4095-b23f-2e2519a1fe02"
}
假設現在access_token過期了,我們用refresh_token去換取新的令牌。使用postman發送如下請求:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjE1NTgyMzEsInVzZXJfbmFtZSI6Im1yYmlyZCIsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImFmNjU5MTE3LWJkMTItNDNmZS04YzE2LTM0MDQxMTMyZDFlOCIsImNsaWVudF9pZCI6InRlc3QxIiwic2NvcGUiOlsiYWxsIl19.4ZD5bXxsXjSw62_1wVl2QpHUKYcC8_1phdNRP02Iihs",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJtcmJpcmQiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiYWY2NTkxMTctYmQxMi00M2ZlLThjMTYtMzQwNDExMzJkMWU4IiwiZXhwIjoxNTYyNDE4NDk3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2MTNjMDVlNS1hNzUzLTRmM2UtOWViOC1hZGE4MTJmY2IyYWQiLCJjbGllbnRfaWQiOiJ0ZXN0MSJ9.e4p3CRyk_cZ82cGzjCBOb4p_0bqRqXElczJjf0nB58o",
"expires_in": 3599,
"scope": "all",
"jti": "af659117-bd12-43fe-8c16-34041132d1e8"
}