回过头来说一下资源服务器的问题点吧,这里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 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 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 map) {
return ProcessPrincipalHelper.converter(map);
}
};
}
public class ProcessPrincipalHelper {
private static final String USER_NAME_KEY = "username";
public static Object converter(Map map) {
Map params = new HashMap();
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 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()方法获取我们自己实现的登录用户信息对象了