前言
当完整的搭建了Spring Security Oauth2 + JWT工程之后,我在想该如何获取当前用户的信息?这本质上算不得是太大问题,配合Redis很容易就能解决,但既然JWT的优点就在于其本身就保存了用户信息,那再去Redis获取就显得多此一举。
这就像是明明身上带着足够的钱,买东西时却偏偏要求你去银行取一样(线上支付让我的例子显得有些苍白无力)。
要做到这一点算不上难,但教材中没有讲述,网上的资源也不多,再加上出现了一些莫名其妙的情况,还是花了我整整一天的时间。我将整理后的流程记录在此,既方便我以后复习,也可供大家参考。
一 从JWT中获取当前用户信息
要从JWT中获取用户信息,需要用到SecurityContextHolder类,如下代码所示:
// 获取用户认证信息。
Authentication getAuthentication = SecurityContextHolder.getContext().getAuthentication();
// 认证信息可能为空,因此需要进行判断。
if (Objects.nonNull(authentication)) {
Object principal = authentication.getPrincipal();
}
principal中即保存着当前用户的信息。
按其他文章的说法,这个时候只需要principal转换成UserDetails对象,便能够获取到相应的对象信息,完整的代码如下:
注:本人实现UserDetails接口的类为UserInfo。
/**
* 获取用户信息
*
* @return
*/
public static UserInfo getUserInfo() {
UserInfo userInfo = null;
// 获取用户认证信息对象。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 认证信息可能为空,因此需要进行判断。
if (Objects.nonNull(authentication)) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserInfo) {
userInfo = (UserInfo) principal;
}
}
return userInfo;
}
不出意外的情况下,这段代码能够已经能够正常获取到用户信息。一切都很简单美好…但经验告诉我不出意外是不可能的。
二 获取用户信息出现的意外情况
只获取到username
这是我遇到的第一个意外情况,当我运行上述方法,最终获取到的userInfo却为null。在排除authentication(用户认证信息对象)为null的情况后,我输出了获取到的principal。
可以看到整个principal中只储存了username,没有任何其他信息。这显然不足以满足开发需求,那么接下来演示如何获取完整的用户信息。
在Spring Security Oauth2 + JWT中,需要配置JwtAccessTokenConverter对象作为操作Token的转换器,所以需要配置此对象的部分信息来达到目的。
JwtAccessTokenConverter默认使用DefaultAccessTokenConverter来处理Token的生成、转换、获取。而DefaultAccessTokenConverter默认使用DefaultUserAuthenticationConverter处理Token中信息的储存、获取。因此,我们只需要重写DefaultUserAuthenticationConverter中关于储存、获取的方法即可。
接下来分析部分源码。为了避免大段的繁复代码,我摘取两段最关键的代码并赋予注释,帮助理解。
/**
* 设置存入认证信息中的Map
*
* @param authentication
* @return
*/
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
// 入参authentication中保存了完整的用户信息。
Map<String, Object> response = new LinkedHashMap();
// 保存了username,因此我们获取到的也是只是username。
response.put("user_name", authentication.getName());
// 保存了账户的权限信息,可以通过Authentication.getAuthorities()方法获取。
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
// 将保存了数据的map返回。
return response;
}
/**
* 选择存入认证信息中的数据
*
* @param map
* @return
*/
public Authentication extractAuthentication(Map<String, ?> map) {
// convertUserAuthentication方法中返回的map会在此处作为入参。
if (map.containsKey("user_name")) {
// 从map中获取用户信息和权限信息,用于之后保存。
Object principal = map.get("user_name");
Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map);
// 若DefaultUserAuthenticationConverter对象中userDetailsService字段不为null,会查询用户并存为用户信息。
// 但奇怪的是即使我手动赋予了实例,也依然只获取到username,有兴趣的同学可以试试。
if (this.userDetailsService != null) {
UserDetails user = this.userDetailsService.loadUserByUsername((String)map.get("user_name"));
authorities = user.getAuthorities();
principal = user;
}
// 将用户信息和权限信息封装为一个Authentication对象。
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
} else {
return null;
}
}
因为extractAuthentication方法默认将username作为用户信息,也因为查询用户信息逻辑莫名失效的缘故(失效的原因暂时未知,可能是我配置错误的原因),因此最终,我们只能获取到username。因此我们将之重写,代码如下。
private static final String USER_INFO = "userInfo";
/**
* 设置存入认证信息中的Map
*
* @param authentication
* @return
*/
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
// 入参authentication中保存了完整的用户信息(都已经有完整信息了还查个P)。
Map<String, Object> map = new HashMap<>(1);
// 获取用户信息并保存。
Object o = authentication.getPrincipal();
UserInfo userInfo = (UserInfo) o;
map.put(USER_INFO, userInfo);
// 保存了账户的权限信息,可以通过Authentication..getAuthorities()方法获取。
if (CollectionUtil.isValid(authentication.getAuthorities())) {
map.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return map;
}
/**
* 选择存入认证信息中的数据
*
* @param map
* @return
*/
public Authentication extractAuthentication(Map<String, ?> map) {
Authentication authentication = null;
if (map.containsKey(USER_INFO)) {
// 将用户对象作为用户信息。
Object principal = map.get(USER_INFO);
Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map);
authentication = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return authentication;
}
改写之后,实例化一个新的DefaultAccessTokenConverter对象,赋予JwtAccessTokenConverter的对象。
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
// DefineUserAuthenticationConverter类是我继承DefaultUserAuthenticationConverter并重写方法的子类。
defaultAccessTokenConverter.setUserTokenConverter(new DefineUserAuthenticationConverter());
// 赋予新的Token转换器。
jwtAccessTokenConverter.setAccessTokenConverter(defaultAccessTokenConverter);
我们再尝试着获取用户信息,输出,等到以下结果:
完整的图片太长,只截取了一部分,但可以看到已经获取到完整的用户信息了。
【附】《Spring Security+OAuth2 + JWT认证以及携带用户信息》
获取到的用户信息无法转换成UserInfo对象
在获取到完整的用户信息之后,出现了新的情况。程序报了ClassCastException异常。原因是获取到的用户信息无法转化为UserInfo对象,因此我输出了一个UserInfo对象进行对比。
UserInfo对象的输出结果:
用户信息的输出结果:
两者的区别还是明显的,也因此出现了转换错误。
既然直接保存的对象会出现缺失而无法被成功的转换,那么JSON字符串呢?如果先将UserInfo对象转换成JSON字符串后保存,再在获取之后进行转换呢?方法是值得尝试一番的。在这里我使用的是阿里的JSON依赖,配置如下(给每个依赖添加注释是一个好习惯,也是一个好素养):
<!-- 阿里巴巴Json依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
改写convertUserAuthentication方法。
/**
* 设置存入认证信息中的Map
*
* @param authentication
* @return
*/
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
// 入参authentication中保存了完整的用户信息(都已经有完整信息了还查个P)。
Map<String, Object> map = new HashMap<>(1);
// 获取用户信息并保存。
Object o = authentication.getPrincipal();
UserInfo userInfo = (UserInfo) o;
String json = JSONObject.toJSONString(userInfo);
map.put(USER_INFO, json);
// 保存了账户的权限信息,可以通过Authentication.getAuthorities()方法获取。
if (CollectionUtil.isValid(authentication.getAuthorities())) {
map.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return map;
}
改写获取用户信息的方法。
/**
* 获取用户信息
*
* @return
*/
public static UserInfo getUserInfo() {
UserInfo userInfo = null;
// 获取用户认证信息对象。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 认证信息可能为空,因此需要进行判断。
if (Objects.nonNull(authentication)) {
Object principal = authentication.getPrincipal();
if (Objects.nonNull(principal)) {
// 狗日的机制,完整的对象都取不出来。
try {
userInfo = JSONObject.parseObject(principal.toString(), UserInfo.class);
} catch (Exception e) {
throw new ResultException(ResultEnum.USER_DETAIL_TRANSITION_ERROR);
}
}
}
return userInfo;
}
工程启动 ,执行,成功 。
一波三折。
注:虽然最终获取到了完整的用户信息,但由于对框架的了解不够全面,可能会导致其它未知的问题,所以我想还是去研究下查询用户信息逻辑莫名失效的原因才是正道。