在spring boot中結(jié)合OAuth2使用JWT時(shí),刷新token時(shí)refresh token一直變化的原因

在spring boot中結(jié)合OAuth2使用JWT時(shí)它掂,客戶端通過 passwordauthorization_code 等方式獲取 access tokenrefresh 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_tokenrefresh_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,都是可以的退腥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末任岸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子狡刘,更是在濱河造成了極大的恐慌享潜,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗅蔬,死亡現(xiàn)場(chǎng)離奇詭異剑按,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)澜术,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門艺蝴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸟废,你說我怎么就攤上這事猜敢。” “怎么了盒延?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵锣枝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我兰英,道長(zhǎng),這世上最難降的妖魔是什么供鸠? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任畦贸,我火速辦了婚禮,結(jié)果婚禮上楞捂,老公的妹妹穿的比我還像新娘薄坏。我一直安慰自己,他們只是感情好寨闹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布胶坠。 她就那樣靜靜地躺著,像睡著了一般繁堡。 火紅的嫁衣襯著肌膚如雪沈善。 梳的紋絲不亂的頭發(fā)上乡数,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音闻牡,去河邊找鬼净赴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛罩润,可吹牛的內(nèi)容都是我干的玖翅。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼割以,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼金度!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起严沥,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤猜极,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后祝峻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魔吐,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年莱找,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酬姆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奥溺,死狀恐怖辞色,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浮定,我是刑警寧澤相满,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站桦卒,受9級(jí)特大地震影響立美,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜方灾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一建蹄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧裕偿,春花似錦洞慎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鸟妙,卻和暖如春焦人,著一層夾襖步出監(jiān)牢的瞬間挥吵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工垃瞧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔫劣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓个从,卻偏偏與公主長(zhǎng)得像脉幢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嗦锐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345