Spring Cloud ~ 从JWT中获取当前用户信息

前言

    当完整的搭建了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;
    }

    工程启动 ,执行,成功 。

    一波三折。

    注:虽然最终获取到了完整的用户信息,但由于对框架的了解不够全面,可能会导致其它未知的问题,所以我想还是去研究下查询用户信息逻辑莫名失效的原因才是正道。


版权声明:本文为qq_39288456原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。