在本系列前面的文章中,正常情況下,OAuth2 傳回的 access_token 資訊一共包含五項:
分别是:
access_token
token_type
refresh_token
expires_in
scope
具體如下:
{
"access_token": "b9c9e345-90c9-49f5-80ab-6ce5ed5a07c9",
"token_type": "bearer",
"refresh_token": "9f843e0e-1778-495d-859a-52a1a806c150",
"expires_in": 7199,
"scope": "seller-auth"
}
但是在實際操作中,我們往往需要在這個基礎上,定制自己的傳回資訊,這就需要我們對這個東西進行自定義。本文松哥就來和大家聊一聊這裡要如何自定義。
敲黑闆劃重點: 本文還是我們最近 OAuth2 系列的延續,如果沒看過本系列之前的文章,一定先閱讀一下,這有助于更好的了解本文:
好了,不廢話了,我們來看今天的内容。
1.access_token 從哪裡來
首先我們要搞清楚,access_token 從哪裡來。
在前面的文章中,我們在生成 access_token 的時候,都配置了一個類,叫做 AuthorizationServerTokenServices,如下:
@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService());
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
TokenEnhancerChain chain = new TokenEnhancerChain();
chain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter,externalAccessTokenInfo));
services.setTokenEnhancer(chain);
return services;
}
在這個配置中,我們提供了一個 DefaultTokenServices 執行個體,這個執行個體就是預設生成 access_token 的工具,我們進入到 DefaultTokenServices#createAccessToken 方法中,一路追蹤,可以看到如下代碼:
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;
}
從這段代碼中,我們可以看到,用來儲存 access_token 的執行個體,其實就是 DefaultOAuth2AccessToken,我們再來看看 DefaultOAuth2AccessToken 的定義:
public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {
private String value;
private Date expiration;
private String tokenType = BEARER_TYPE.toLowerCase();
private OAuth2RefreshToken refreshToken;
private Set scope;
private Map additionalInformation = Collections.emptyMap();
//省略其他
}
從這段屬性的聲明中,我們就可以看出來,為什麼預設傳回的資料隻有五項。
大家同時也發現,DefaultOAuth2AccessToken 中其實是提供了一個 additionalInformation 屬性用來存儲額外資訊的,但是,我們在 DefaultTokenServices 類中并沒有辦法去自定義 DefaultOAuth2AccessToken 中的屬性,也就是說,預設情況下,我們沒有辦法自己去給 additionalInformation 中添加值。
雖然預設情況下,無法添加,但是隻要大家看了上面這段源碼,就會明白,如果我們想要自定義傳回的 access_token 資訊,就要想辦法自已去定義 DefaultOAuth2AccessToken 資訊。
思路有了,接下來看操作。
2.兩種定制方案
大家知道,我們在 OAuth2 中傳回的令牌資訊分為兩大類:不透明令牌和透明令牌。
不透明令牌就是一種無可讀性的令牌,一般來說就是一段普通的 UUID 字元串。不透明令牌的最大問題在于會降低系統性能和可用性,并且增加延遲(因為必須遠端校驗令牌)。
透明令牌的典型代表就是 JWT 了,使用者資訊都儲存在 JWT 字元串中,關于 JWT 的資訊,大家可以參考這篇文章:想讓 OAuth2 和 JWT 在一起愉快玩耍?請看松哥的表演。
在實際開發中,大部分情況下,我們的 OAuth2 都是搭配 JWT 一起來使用的,是以,這裡我就主要講一下在生成的 JWT 中如何定制傳回資訊。
如果我們使用了 OAuth2+JWT 的方案,那正常情況下,我們還需要配置一個 JwtAccessTokenConverter 的執行個體(參考:想讓 OAuth2 和 JWT 在一起愉快玩耍?請看松哥的表演),JWT 字元串将由 JwtAccessTokenConverter 執行個體負責生成。
JwtAccessTokenConverter 執行個體生成 JWT 的方法是在上文列出來的 DefaultTokenServices#createAccessToken 方法之後執行,該方法最後有一句:
accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
這就是說,如果提供了 accessTokenEnhancer ,就進入到 accessTokenEnhancer 的 enhance 方法中對 access_token 做二次處理,accessTokenEnhancer 則就是我們的 JwtAccessTokenConverter 執行個體。
從這裡大家看到,想要自定義 Token 資訊,我們有兩個時機,第一個時機就是在 DefaultTokenServices#createAccessToken 方法中修改,但是工作量較大,不推薦;第二個時機是在進入到 JwtAccessTokenConverter#enhance 方法之後修改,這是目前比較可行的方法。
如果采用第二種方案,就需要我們自定義一個類繼承自 JwtAccessTokenConverter,如下:
public class MyJwt extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map additionalInformation = new LinkedHashMap<>();
Map info = new LinkedHashMap<>();
info.put("author", "江南一點雨");
info.put("email", "[email protected]");
info.put("site", "www.javaboy.org");
info.put("weixin", "a_java_boy2");
info.put("WeChat Official Accounts", "江南一點雨");
info.put("GitHub", "https://github.com/lenve");
info.put("user", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
additionalInformation.put("info", info);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
return super.enhance(accessToken, authentication);
}
}
在這裡,我們自定義 MyJwt 繼承自 JwtAccessTokenConverter 并重寫 enhance 方法:
首先我們構造自己的附加資訊,如果如需要目前登入使用者資訊,可以從 SecurityContextHolder 中擷取。
将附加資訊放到 OAuth2AccessToken 的 additionalInformation 屬性中去。
這樣相當于我們就修改了預設生成的 DefaultOAuth2AccessToken 了,然後再把修改後的 DefaultOAuth2AccessToken 執行個體調用 super.enhance 方法去生成 jwt 字元串,這樣生成的 jwt 字元串就有我們的自定義資訊了。
最後,在 TokenConfig 中配置 MyJwt 的執行個體,如下:
@Configuration
public class TokenConfig {
@Bean
TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new MyJwt();
converter.setSigningKey("www.javaboy.org");
return converter;
}
}
配置完成後,其他地方的代碼不變(參考:想讓 OAuth2 和 JWT 在一起愉快玩耍?請看松哥的表演),我們啟動項目來生成登入的 access_token 資訊。
3.測試
接下來,我們啟動項目進行測試:
可以看到,此時生成的 jwt 字元串就比較長了,我們将 access_token 拿到 /oauth/check_token 去校驗一下就知道生成的具體資訊了,如下:
可以看到,我們已經成功的将自定義資訊存入 jwt 字元串中了。
當然,還有一種情況就是你可能隻是想在調用 /oauth/token 接口的時候添加一些額外資訊,并不想将額外資訊添加到 jwt 中,就是下面這種效果:
4.擴充
好了,前面雖然跟大家分享的是 OAuth2+JWT 如何生成自定義的 access_token 資訊,但是相信大家看完之後,應該也會針對不透明令牌生成自定義資訊。
我這裡也和大家分享一下思路:
上面代碼的核心思路,就是在從 DefaultTokenServices#createAccessToken 方法到 JwtAccessTokenConverter#enhance 方法的過程中,給 DefaultOAuth2AccessToken 對象的 additionalInformation 屬性添加了附加資訊。
而 JwtAccessTokenConverter 是 TokenEnhancer 的執行個體,是以如果我們想要定制不透明令牌的資訊,隻需要自己定義類實作 TokenEnhancer 接口,并且在 enhance 方法中添加附加資訊即可。這個思路給大家,小夥伴們可以自行嘗試一下。
好了,今天就和大家分享這麼多。如果覺得有收獲,記得點個在看鼓勵下松哥哦~
最後再說一下,如果你覺得閱讀本文有些吃力,一定閱讀一下本系列前面的文章。
關注微信公衆号江南一點雨,回複 2020 擷取 SpringSecurity+OAuth2幹貨!