在spring boot中結(jié)合OAuth2使用JWT時(shí)它掂,客戶端通過 password
或 authorization_code
等方式獲取 access token
和 refresh token
巴帮,并通過 refresh token
來進(jìn)行續(xù)約。但當(dāng)客戶端刷新token時(shí)虐秋,我們發(fā)現(xiàn)認(rèn)證服務(wù)總是返回新的refresh token榕茧,這是什么原因呢?
一客给、場(chǎng)景展現(xiàn)
1. 獲取token
curl -u barClientIdPassword:secret http://localhost:8081/oauth/token -d grant_type=password -d username=user -d password=password -d scope=bar+read
返回的結(jié)果如下:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyLXJlc291cmNlIl0sImNyZWF0ZV90aW1lIjoxNTEwNDE2MDAwMDAwLCJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYmFyIiwicmVhZCJdLCJleHAiOjE1MTA1OTM2MjUsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI2NGM0ZDQzYi0zNmNjLTQ1ZTQtOGViMy1iMGM1MmY0M2U0MTQiLCJjbGllbnRfaWQiOiJiYXJDbGllbnRJZFBhc3N3b3JkIn0.E0aQP27ccVrABvukElcYrcp5gpTEED2YLxNn1P_bkdIQOzdo3BEb30s0WP3cUEOrz56VHYmQ0_hC9xD1seF5zDk9zt1DJBvNsx-czZ3Rprg_v6l5MosoljZzhQjMX2LUVRW5WBX9sHF348yL3WZzpofgycxDdPOYgiDdxTRmCBJLYq-Jed5vIF94OVGbFrwHeXHWPqp7IKDS33uaSu1ISnXSJPHze5KPW237R83DmLCihG14GqNF4c6W9db4ODPLCUavXUUQGxN6YwM0FOQJKxRupj61HsTePYNIKdd2D0O6orUvxx2o-op-U2_ImmTBYDOElM2Raep3CnmBhofvNg",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyLXJlc291cmNlIl0sImNyZWF0ZV90aW1lIjoxNTEwNDE2MDAwMDAwLCJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYmFyIiwicmVhZCJdLCJhdGkiOiI2NGM0ZDQzYi0zNmNjLTQ1ZTQtOGViMy1iMGM1MmY0M2U0MTQiLCJleHAiOjE1MTA1OTM2MjMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI5ZTE1MDhjMy1mMDcxLTRiZmMtODQ4ZC0xNWIxMzRmZjZiYmYiLCJjbGllbnRfaWQiOiJiYXJDbGllbnRJZFBhc3N3b3JkIn0.bpHO-2xx322dE5cZvdrEpw2L1LXMNE1hnC8wr3dA_cwaHhuHe9MTaJRS_itfTTIQIEnxQAYnZIj60C9fTUhB166n9bm996-b10zWREqqM-tgrU_RurG2bwqLawx6OheJms_sK1-vMneT1EqhZ54GXhtm5ulxuINjVxxYNnkJtRF2dMJPk4vSd6ay-XhSmxaEabqePN9RR5i15PfcF8apLn4ENY1TeVOGLecWle5c5AXL98iKiiE8AtuWWJ1WNWQ0CdtMzdBEf__sQ_T4dU6VE6G7aHG0s_l_fE7sbgzhOUMq39so51FiF0IUk3B83q6MnPpEALwj0BQUWRMqEdy88w",
"expires_in": 35690,
"scope": "bar read",
"create_time": 1510416000000,
"jti": "64c4d43b-36cc-45e4-8eb3-b0c52f43e414"
}
其中:
-
create_time
為自定義屬性 -
access_token
和refresh_token
是經(jīng)過簽名并Base64編碼后的一個(gè)字符串用押。
我們來看下 refresh token
在編碼前的結(jié)構(gòu),它包含的內(nèi)容大致如下:
{
aud:[oauth2-resource], // token所對(duì)應(yīng)的resourceid
create_time:1510416000000, // 自定義的屬性
user_name:user,
scope:[bar, read],
ati:64c4d43b-36cc-45e4-8eb3-b0c52f43e414, // access token id
exp:1510593623,
authorities:[ROLE_USER],
jti:9e1508c3-f071-4bfc-848d-15b134ff6bbf, // token id
client_id:barClientIdPassword
}
2. 刷新token
在第1步我們獲取到了 refresh token
靶剑,這時(shí)我們拿著 refresh token
向認(rèn)證服務(wù)器申請(qǐng)新的 access token
:
curl -u barClientIdPassword:secret http://localhost:8081/oauth/token -d grant_type=refresh_token -d refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyLXJlc291cmNlIl0sImNyZWF0ZV90aW1lIjoxNTEwNDE2MDAwMDAwLCJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYmFyIiwicmVhZCJdLCJhdGkiOiI2NGM0ZDQzYi0zNmNjLTQ1ZTQtOGViMy1iMGM1MmY0M2U0MTQiLCJleHAiOjE1MTA1OTM2MjMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI5ZTE1MDhjMy1mMDcxLTRiZmMtODQ4ZC0xNWIxMzRmZjZiYmYiLCJjbGllbnRfaWQiOiJiYXJDbGllbnRJZFBhc3N3b3JkIn0.bpHO-2xx322dE5cZvdrEpw2L1LXMNE1hnC8wr3dA_cwaHhuHe9MTaJRS_itfTTIQIEnxQAYnZIj60C9fTUhB166n9bm996-b10zWREqqM-tgrU_RurG2bwqLawx6OheJms_sK1-vMneT1EqhZ54GXhtm5ulxuINjVxxYNnkJtRF2dMJPk4vSd6ay-XhSmxaEabqePN9RR5i15PfcF8apLn4ENY1TeVOGLecWle5c5AXL98iKiiE8AtuWWJ1WNWQ0CdtMzdBEf__sQ_T4dU6VE6G7aHG0s_l_fE7sbgzhOUMq39so51FiF0IUk3B83q6MnPpEALwj0BQUWRMqEdy88w
返回結(jié)果:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyLXJlc291cmNlIl0sImNyZWF0ZV90aW1lIjoxNTEwNDE2MDAwMDAwLCJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYmFyIiwicmVhZCJdLCJleHAiOjE1MTA1OTM5OTMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI5ZmUwZjRmZC1iMDI4LTQxODktYjVlZS1lOWQxYjgzYzU3NDIiLCJjbGllbnRfaWQiOiJiYXJDbGllbnRJZFBhc3N3b3JkIn0.gWs0uXa20k-WcfBcXWmz4xbZ-_VtyxTcIxDHlvNm0ziB3vvh0BxaaV7wEqVXePgA10Hm5Z42J-wAIinx7BmuRIs58pm_5t7FqEA_4XTL1bcMkvJpLEqhH5q6VFnqUxp4URC0l4l7jG8JfdZaD4Xr0Y6M7Aviu5OlWRf4jS57SeVxt41hbZvoSQjoQL-4hxDsPyNkSuBcIL237p4HqvAON-1eU0o1OYlj2u2OE4hSs4pnutWTb_ooNo0JSvcm4y9xPDKTzPg0Gb0F9UWdYRNwTd_LtD10VgutDQUqTZ5R_2r-kzUGqyT2ynVoXLc8iXjZ26g7552L3-R6pUMsrCq9FQ",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyLXJlc291cmNlIl0sImNyZWF0ZV90aW1lIjoxNTEwNDE2MDAwMDAwLCJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYmFyIiwicmVhZCJdLCJhdGkiOiI5ZmUwZjRmZC1iMDI4LTQxODktYjVlZS1lOWQxYjgzYzU3NDIiLCJleHAiOjE1MTA1OTM2MjMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI5ZTE1MDhjMy1mMDcxLTRiZmMtODQ4ZC0xNWIxMzRmZjZiYmYiLCJjbGllbnRfaWQiOiJiYXJDbGllbnRJZFBhc3N3b3JkIn0.ZPLpF--gqunfjQTi4TrrP6Mw043U-dz9iuHwLOEVdjog1lmWmWVcMbvdDJnw2Vh_zBRAlJFEchUpySacQyyBmpqJTxTzOKoYYDkhHe7L-BpPbgNmucbIIVEwP9GnMWnAQb8t_kJCTJXc3h31j6lnTVqhcX-rVpON-L9gk-qvSIstwPN8B39ESOVQ6gb8LgkawRwtIP7fDiDoVon3bdWsI8CKZLj4dKwoXDj6MXDj-2IISbnNCM_E-H_b5TnFjB-gRX3cqwh3VkIdXc5o9048zVlxgRJxzXJgxhTzU0zZFVRPPzVA90gnrXuZBLLMJSPRDpCnhgSU8TaeRkedJLGt8g",
"expires_in": 32300,
"scope": "bar read",
"create_time": 1510416000000,
"jti": "9fe0f4fd-b028-4189-b5ee-e9d1b83c5742"
}
我們可以看到蜻拨,返回結(jié)果中的 refresh token
串和請(qǐng)求時(shí)的refresh token
發(fā)生了變化,這和我們預(yù)期的不一樣桩引,到底是什么原因呢缎讼?
二. 原因分析
2.1 代碼分析
首先來看認(rèn)證服務(wù)器的配置代碼:
@EnableAuthorizationServer
@Configuration
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
// 注入認(rèn)證管理器
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用jdbc存儲(chǔ)客戶端信息
clients.withClientDetails(clientDetails());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//指定認(rèn)證管理器
endpoints.authenticationManager(authenticationManager);
//指定token存儲(chǔ)位置
endpoints.tokenStore(tokenStore());
// 自定義token生成方式
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customerEnhancer(), accessTokenConverter()));
endpoints.tokenEnhancer(tokenEnhancerChain);
// 配置TokenServices參數(shù)
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
// 復(fù)用refresh token
tokenServices.setReuseRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 1天
endpoints.tokenServices(tokenServices);
super.configure(endpoints);
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
return converter;
}
因?yàn)橛玫搅薚okenEnhancer和JwtAccessTokenConverter,順藤摸瓜找到關(guān)鍵代碼
關(guān)鍵代碼一
org.springframework.security.oauth2.provider.token.DefaultTokenServices類:
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
關(guān)鍵代碼二
org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter類:
// 此方法在獲取token或刷新token時(shí)都會(huì)被調(diào)用
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
String tokenId = result.getValue();
if (!info.containsKey(TOKEN_ID)) {
info.put(TOKEN_ID, tokenId);
}
else {
tokenId = (String) info.get(TOKEN_ID);
}
result.setAdditionalInformation(info);
result.setValue(encode(result, authentication));
// 1. 在獲取token時(shí)坑匠,取得的refreshToken時(shí)為服務(wù)端剛生成的原始的jti值(格式類似9e1508c3-f071-4bfc-848d-15b134ff6bbf)
// 2. 在刷新token時(shí)血崭,取得的refreshToken為客戶端發(fā)過來的加密之后refresh token串
OAuth2RefreshToken refreshToken = result.getRefreshToken();
if (refreshToken != null) {
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
encodedRefreshToken.setValue(refreshToken.getValue());
// Refresh tokens do not expire unless explicitly of the right type
encodedRefreshToken.setExpiration(null);
// 假設(shè)前面取得的refreshToken為客戶端發(fā)過來的加密后的token,嘗試解密并從里面取原始的jti厘灼,這里有兩種可能:
// 1. 在獲取token時(shí)夹纫,解密失敗,拋出異常手幢,所以refreshToken還是為原始的jti捷凄;
// 2. 在刷新token時(shí),解密成功围来,取得原始的jti
try {
Map<String, Object> claims = objectMapper
.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey(TOKEN_ID)) {
// 取得原始的jti跺涤,并賦值給encodedRefreshToken
encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
}
}
catch (IllegalArgumentException e) {
}
Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
accessToken.getAdditionalInformation());
refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue()); // 原始的token(jti)保持不變
refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId); // 新的access token id(ati)
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
encode(encodedRefreshToken, authentication));
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
// 加密生成新的refresh token串(其實(shí)里面的jti還是原來的值)
token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
}
result.setRefreshToken(token);
}
return result;
}
2.2 結(jié)論
刷新token時(shí)匈睁,token id(jti)其實(shí)是保持不變的,只是因?yàn)橹匦律闪薬ccess token(即ati)桶错,所以加密之后的refresh token串看起來和原來的不一樣了航唆,實(shí)際上里面的jti還是一樣的。
此時(shí)我們可以發(fā)現(xiàn)院刁,只要refresh token沒有過期糯钙,不管是使用原來的refresh token還是新的refresh token去刷新token,都是可以的退腥。