Spring Security 源碼分析(五):JWT 實(shí)現(xiàn)

JWT

JWT(Json Web Token) 是一個(gè)開(kāi)放標(biāo)準(zhǔn),它定義了一種緊湊和自包含的方式,用于在各方之間作為 JSON 對(duì)象安全地傳輸信息。

  • 緊湊:token 值是一個(gè)很小的 Base64 編碼的字符串,可以通過(guò) http 請(qǐng)求參數(shù)或者 header 傳遞。
  • 自包含:token 可以包含很多信息,包括用戶名、權(quán)限、過(guò)期時(shí)間等,支持開(kāi)發(fā)者自定義。

通常一個(gè) JWT 字符串的解析結(jié)果如下:

JWT編碼解碼

JWT 串由 3 部分組成:

  • header:頭部,用于標(biāo)識(shí) tokenJWT 類型和使用的簽名算法。

  • payload:有效數(shù)據(jù),JWT 自包含的信息。

  • signature:對(duì)頭部和有效信息的簽名。

因此 JWT 能夠安全地傳輸安全信息。

使用 JWT 替換默認(rèn) token 實(shí)現(xiàn)

Spring Security 提供了諸多的 TokenStore 實(shí)現(xiàn),如存在內(nèi)存中的 InMemoryTokenStore 、存在數(shù)據(jù)庫(kù)中的 JdbcTokenStore、存在 Redis 中的 RedisTokenStore,這些都是通過(guò)將生成的 token 存儲(chǔ)下來(lái),當(dāng)?shù)谌綉?yīng)用請(qǐng)求受保護(hù)資源部時(shí),會(huì)去 TokenStore 查詢是否有相應(yīng)的令牌。僅將令牌存儲(chǔ)在內(nèi)存中不支持分布式環(huán)境;存儲(chǔ)在數(shù)據(jù)庫(kù)或 Redis 中,每次請(qǐng)求都去查詢又會(huì)增加后端的負(fù)擔(dān);一旦服務(wù)器宕機(jī),勢(shì)必又要影響用戶訪問(wèn)。而 JWT 對(duì)于令牌的實(shí)現(xiàn)由于自包含的特性,能有效解決上述問(wèn)題。

JwtTokenStore

無(wú)論是 4 種授權(quán)方式的哪一種,在授權(quán)認(rèn)證完成后,都是通過(guò)在 AbstractTokenGranter 中調(diào)用 AuthorizationServerTokenServices#createAccessToken 方法頒發(fā)令牌的,JWT 由于其自包含的特性,是不會(huì)存儲(chǔ)在后端應(yīng)用中的,因此每次都需要申請(qǐng)授權(quán)都會(huì)直接創(chuàng)建新的令牌。普通令牌中只有 scope、refresh_token 等基本信息,JWT 如何實(shí)現(xiàn)其自包含特性呢?在創(chuàng)建令牌時(shí),DefaultTokenServices#createAccessToken 方法使用了 TokenEnhancerJWT 中添加附加信息:

    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        // ...
        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }

因此需要配置 JwtAccessTokenConverter 來(lái)增強(qiáng) JWT 的構(gòu)成。

JwtAccessTokenConverter

在授權(quán)端點(diǎn)配置 AuthorizationServerEndpointsConfigurer 中,我們可以配置 JwtAccessTokenConverter

  public AuthorizationServerEndpointsConfigurer accessTokenConverter(AccessTokenConverter accessTokenConverter) {
    // 配置 JwtAccessTokenConverter
    this.accessTokenConverter = accessTokenConverter;
    return this;
  }
  private TokenEnhancer tokenEnhancer() {
    if (this.tokenEnhancer == null && accessTokenConverter() instanceof JwtAccessTokenConverter) {
      // JwtAccessTokenConverter 也實(shí)現(xiàn)了 TokenEnhancer 接口
        tokenEnhancer = (TokenEnhancer) accessTokenConverter;
    }
    return this.tokenEnhancer;
  }

  private TokenStore tokenStore() {
    if (tokenStore == null) {
      if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
        // 如果配置了 JwtAccessTokenConverter,那么配置 JwtTokenStore
        this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
      } else {
        this.tokenStore = new InMemoryTokenStore();
        }
    }
    return this.tokenStore;
  }

一旦設(shè)置了 JwtAccessTokenConverter 就可以默認(rèn)配置 tokenEnhancer,并將 tokenStore 設(shè)置為 JwtTokenStore。JwtAccessTokenConverter#enhance 方法中對(duì)于增加 JWT 附加信息的邏輯如下:

  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)) {
      // 增加 jti,即授權(quán)服務(wù)器生成的原始訪問(wèn)令牌字符串
      info.put(TOKEN_ID, tokenId);
    } else {
      tokenId = (String) info.get(TOKEN_ID);
    }
    result.setAdditionalInformation(info);
    // 按照 JWT 生成算法拼裝 JWT
    result.setValue(encode(result, authentication));
    OAuth2RefreshToken refreshToken = result.getRefreshToken();
    if (refreshToken != null) {
      // 拼接刷新令牌的 JWT
      // ...
    }
    return result;
  }

在對(duì) payload 部分編碼時(shí)調(diào)用了 DefaultAccessTokenConverter#convertAccessToken 方法:

  public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
    // ...
        // 增加用戶名及其權(quán)限信息
    if (!authentication.isClientOnly()) {
      response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));
    } else {
      if (clientToken.getAuthorities()!=null && !clientToken.getAuthorities().isEmpty()) {
        response.put(UserAuthenticationConverter.AUTHORITIES,
                     AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));
      }
    }
        // 增加 scope 信息
    if (token.getScope()!=null) {
      response.put(scopeAttribute, token.getScope());
    }
    // 增加原始訪問(wèn)令牌
    if (token.getAdditionalInformation().containsKey(JTI)) {
      response.put(JTI, token.getAdditionalInformation().get(JTI));
    }
        // 增加令牌過(guò)期時(shí)間
    if (token.getExpiration() != null) {
      response.put(EXP, token.getExpiration().getTime() / 1000);
    }
    // 增加授權(quán)類型
    if (includeGrantType && authentication.getOAuth2Request().getGrantType()!=null) {
      response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType());
    }
        // 增加其他附加信息
    response.putAll(token.getAdditionalInformation());
        // ...
    return response;
  }

JWT 的加密、解密是通過(guò) Spring Security 提供的工具 JwtHelper 實(shí)現(xiàn)的,開(kāi)發(fā)者可以自定義秘鑰。

TokenKeyEndpoint

關(guān)于 JWT,Spring Security 還留有一個(gè)彩蛋:在配置授權(quán)端點(diǎn)時(shí),引入了 TokenKeyEndpointRegistrar 配置,當(dāng) Spring 容器中有 JwtAccessTokenConverter 實(shí)例時(shí)會(huì)注冊(cè) TokenKeyEndpoint,此配置提供了 /oauth/token_key 接口用于查詢生成 JWT 簽名的算法以及用于驗(yàn)證的密鑰。

/oauth/token_key 接口默認(rèn)拒絕任何訪問(wèn)請(qǐng)求,通過(guò)授權(quán)服務(wù)器安全配置擴(kuò)展設(shè)置接口的訪問(wèn)權(quán)限:

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  security.tokenKeyAccess("authenticated");
}

小結(jié)

  • JWT 是一種緊湊和自包含的 token 實(shí)現(xiàn)方式,其有效數(shù)據(jù)包含了用戶的認(rèn)證信息,并通過(guò)加密簽名來(lái)保證安全性。
  • JWT 可以存儲(chǔ)在客戶端,由于其無(wú)狀態(tài)特性,天然支持分布式。由于自包含有效數(shù)據(jù),避免了每次訪問(wèn)資源服務(wù)器都需要查詢后端數(shù)據(jù)。
  • Spring Security 中通過(guò)配置 JwtAccessTokenConverter 來(lái)使用 JWT。開(kāi)發(fā)者可以干預(yù) JWT 中的附加信息和加密方式等。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容