springCloud微服務系列——OAuth2+JWT模式下的【資源伺服器】獲得【自定義資訊】
回過頭來說一下資源伺服器的問題點吧,這裡OAuth2+JWT用的是spring security,具體怎麼用spring security搭建資源伺服器我就不說了。這裡要討論的問題是這樣的,我們希望在spring mvc中,直接通過如下的形式獲得登入使用者資訊
@GetMapping("/me")
public Authentication me(OAuth2Authentication user) {
ProcessUser principal = (ProcessUser) user.getPrincipal(); // 獲得自己實現的類
return user;
}
其中ProcessUser是我們自己實現的類,包含了通過tokenEnchance擴充套件的自定義使用者資訊。
一、spring security資源伺服器原始碼解析
為了找到實現的思路,我們需要分析原始碼,找尋線索。和之前的文章一樣,我把執行的關鍵部分總結成了一張圖,有興趣的可以自己打斷點跟蹤一遍。這裡要吐槽一下,本來用寫得畫的圖,畫到一般出bug了,所以後面一部分用的excel
大體的流程再總結一下:資源認證伺服器filter->AuthenticationManager->DefaultTokenServices->JwtTokenStore->JwtAccessTokenConverter->DefaultAccessTokenConverter
大體思路也總結一下:從http頭中取出token值->封裝成accessToken物件->解密為map物件->通過DefaultAcccessTokenConverter進行解析map,轉換成OAuth2Authentication物件
二、解決方案
通過原始碼分析,我們已經知道最後轉換成OAuth2Authentication物件是通過DefaultAcccessTokenConverter的userTokenConverter,而DefaultAcccessTokenConverter則是JwtAccessTokenConverter預設的呼叫物件,我們可以直接用DefaultAccessToken,只是把它的userTokenConverter換成我們的即可。
因此我們可以繼承JwtAccessTokenConverter,覆寫extractAuthentication方法,在其中設定accessTokenConverter的userTokenConverter為我們自定義的Converter。最後再把我們實現的JwtAccessTokenConverter注入到spring中。
1、實現我們的userTokenConverter
public class AdditionalUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
private Collection<? extends GrantedAuthority> defaultAuthorities;
private UserDetailsService userDetailsService;
private PrincipalConverter principalConverter;
public AdditionalUserAuthenticationConverter(PrincipalConverter principalConverter) {
this.principalConverter = principalConverter;
}
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = principalConverter.converter(map);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
if (!map.containsKey(AUTHORITIES)) {
return defaultAuthorities;
}
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
通過這句話Object principal = principalConverter.converter(map);實現轉換成我們自己的物件。principalConverter是為了保證通用性而制定的一個介面,具體專案,實現該介面,註冊到spring中就可以了。
@Bean
public PrincipalConverter principalConverter() {
return new PrincipalConverter() {
@Override
public Object converter(Map<String, ?> map) {
return ProcessPrincipalHelper.converter(map);
}
};
}
public class ProcessPrincipalHelper {
private static final String USER_NAME_KEY = "username";
public static Object converter(Map<String, ?> map) {
Map<String, Object> params = new HashMap<String, Object>();
for(String key : map.keySet()) {
params.put(key, map.get(key));
}
ProcessUser processUser = new ProcessUser((String) map.get(USER_NAME_KEY));
return BeanUtils.mapToBean(params, processUser);
}
}
2、繼承JwtAccessTokenConverter
主要是覆寫extractAuthentication方法,還是用DefaultAccessTokenConverter處理邏輯,只是把它的userTokenConverter換成我們的實現即可
public class AdditionalJwtAccessTokenConverter extends JwtAccessTokenConverter {
private DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
private PrincipalConverter principalConverter;
public AdditionalJwtAccessTokenConverter(PrincipalConverter principalConverter) {
this.principalConverter = principalConverter;
}
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
UserAuthenticationConverter userAuthenticationConverter =
new AdditionalUserAuthenticationConverter(principalConverter);
accessTokenConverter.setUserTokenConverter(userAuthenticationConverter);
return accessTokenConverter.extractAuthentication(map);
}
}
3、注入我們實現的JwtAccessTokenConverter
@Bean
@ConditionalOnMissingBean(name = "jwtAccessTokenConverter")
public JwtAccessTokenConverter jwtAccessTokenConverter() {
AdditionalJwtAccessTokenConverter jwtAccessTokenConverter = new AdditionalJwtAccessTokenConverter(principalConverter());
jwtAccessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey());
return jwtAccessTokenConverter;
}
至此,我們就可以通過OAuth2Authentication物件的getPrincipal()方法獲取我們自己實現的登入使用者資訊物件了