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 串由 3 部分組成:
header:頭部,用于標(biāo)識(shí)token為JWT類型和使用的簽名算法。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 方法使用了 TokenEnhancer 向 JWT 中添加附加信息:
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中的附加信息和加密方式等。