美文网首页
全局获取用户信息

全局获取用户信息

作者: hekirakuno | 来源:发表于2019-06-18 17:12 被阅读0次

    对于当前登录用户,我们其实经常有获取它的一些简单的基本信息的需求,如果次次都要查数据库,其实会造成性能浪费,也很麻烦。而且,在某些接口中,并不要求参数中传值一些当前用户的信息。

    举个例子:日记网站--更新当前登录用户某天日记的接口。

    对于业务需求来说,我的前端会认为(较合理)我在更新我自己的日记,我为什么要给你传我自己的用户编码,你应当知道,这样才对啊。

    况且更新当前用户信息的时候其实也是不能让前端传id的,因为会被攻击,比如我知道了另外一个人的用户id,就可以通过篡改id来修改其他人的信息了。一般来说,我们会在后台维护一个全局的缓存信息,但是在更新用户的时候需要同步更新缓存,当前用户的信息都是禁止前端传的,有风险。

    那么,为了实现这种需求,我们要怎么做呢?
    我想过如下的几种操作。
    (1)把当前用户信息压进jwt,然后调用JwtUtil中的解析方法获取需求的claim信息;因为jwt中,本身就可以封装一些基本信息,通常我们都会封进去用户id,然后在鉴权的时候拿到它作为key,比对redis中的token,达到鉴权认证的功能;
    (2)把用户信息放进redis,在用户登录的时候,除了把token塞进redis以外,同时把用户基本信息塞进redis,然后每次使用时都去redis查;
    (3)把用户信息放进threadLocal中,使用时调用它的静态方法,可以在后台代码中直接获取。

    我一开始尝试了这样的操作,(1)+(3)搭配。因为每次调用接口的时候都会走jwtFilter的认证服务,在那里会比对token的正确性与时效性,那么,我去取出jwt中封装的用户信息直接塞进ThreadLocal,在这次线程调用的范围内,可以保障之后所涉及的步骤里,都可以通过它的静态方法,获取到封装在jwt中的用户信息。但是很快,这种方案就被我腰斩了,因为jwt中,事实上还是不要附带太多用户信息比较好,其一,暴露用户资料,其二,如果我直接从jwt中解析其中附带的信息,很可能会因为用户更新了资料而token尚未更新造成数据不一致的问题。因此,我选择的是结合(2)+(3)的方案。
    完整的过程如下:

    (1)登录成功时,我会在redis中缓存两条数据,拼接前缀1/后缀1+id组成的key和token的value(带过期时间),拼接前缀2/后缀2+id组成的key和用户基本信息组成的value(不设置过期时间);
    (2)更新用户信息时,同时更新用户信息的redis缓存数据;
    (3)调用接口时,获取前端塞进请求头header传来的token,比对正确性与时效性。如果正确直接放行;如果过期,则用该token获取用户id,拼接前缀1/后缀1+id,去redis中查找为该key值的token,如果匹配上,则更新token,塞入header返回前端,让它更新,如果不匹配则返回401错误;如果token不正确,直接返回401错误;
    (4)接(3)细节,如果正确,则用该token获取用户id,拼接前缀2/后缀2+id,去redis查找为该key值的用户信息,塞进ThreadLocal,可供这次线程调用过程中的任何地方使用该全局信息;如果过期,则刷新redis中的token的时候,同时刷新用户信息(如果有人手改数据库,没有走接口update,这里给了一个调整强一致性的保障。)
    (5)其实尽管这么处理了,因为缓存的缘故,总可能出现数据不一致的情况,但是我们保障不可同时登陆同一个账号,更新操作最终一致性,不通过前端传值用户id直接篡改数据库,在一定程度上保障了速度与安全的比例。

    简单的jwt的设计(设计中属性只有token):

    public class JWTToken implements AuthenticationToken {
        // 密钥
        private String token;
    
        public JWTToken(String token) {
            this.token = token;
        }
    
        @Override
        public Object getPrincipal() {
            return token;
        }
    
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    
    

    把userNum和userName封装进jwt中(claim中可以增加很多很多):

    public static String sign(UserInfoVo userInfoVo) {
            try {
                //token过期时间
                Date date = new Date(System.currentTimeMillis() + (Long.parseLong(tokenExpireTime) * 60 * 1000));
                //密码MD5加密
                Algorithm algorithm = Algorithm.HMAC256(userInfoVo.getPassword());
                // usernum信息
                //删掉.withClaim("userName", userInfoVo.getUserName()),只在token中带上userNum(用户id)就可以了
                return JWT.create()
                        .withClaim("userNum", userInfoVo.getUserNum()).withExpiresAt(date).sign(algorithm);
            } catch (Exception e) {
                log.error("生成签名异常:{}", e);
                return null;
            }
        }
    

    获取userNum的方法(同理可以获取其他信息):

    public static String getUserNum(String token) {
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("userNum").asString();
            } catch (JWTDecodeException e) {
                log.error("获取token中用户编码信息异常:{}", e);
                return null;
            }
        }
    

    threadLocal线程变量工具

    @Slf4j
    public class SessionLocal {
        private static ThreadLocal<UserInfoVo> local = new ThreadLocal<UserInfoVo>();
    
        /**
         * 设置用户信息
         *
         * @param userInfo
         */
        public static void setUserInfo( UserInfoVo userInfo )
        {
            local.set(userInfo) ;
            log.info("存入用户信息:{}",userInfo);
        }
    
        /**
         * 获取登录用户信息
         *
         * @return
         */
        public static UserInfoVo getUserInfo()
        {
            log.info("当前线程id:{}",Thread.currentThread().getName());
            return local.get();
        }
        /**
         * 删除掉储存的用户信息
         */
        public static void remove(){
            local.remove();
        }
    }
    

    redis存储数据:

    @Override
        public void addTokenToRedis(String userNum, String jwtTokenStr) {
            //userNum是唯一的。
            String key = CommonConstant.JWT_TOKEN + userNum ;
            //集合类型不存在设置每一个key的过期时间,所以其实最后还是只能用string类型。
            //redisTemplate.opsForHash().put("token",key,jwtTokenStr);
            redisTemplate.opsForValue().set(key, jwtTokenStr, refreshJwtTokenExpireTime, TimeUnit.MINUTES);
        }
    
        @Override
        public void addUserInfoToRedis(String userNum, UserInfoVo userInfoVo) {
            String key = CommonConstant.USER_SIMPLE_INFO + userNum ;
            redisTemplate.opsForValue().set(key,userInfoVo);
    }
    

    登录接口:

    /**
         * 登陆
         * shiro+jwt登录。
         *
         */
        @PostMapping("/login")
        @ApiOperation(value = "登录", notes = "用户登录接口")
        @ApiResponses({
                @ApiResponse(code = 80000,message = "登录失败",response = ApiResult.class),
                @ApiResponse(code = 80001,message = "用户名或密码错误",response = ApiResult.class)
        })
        public ApiResult login(@RequestBody UserInfoDto userInfoDto) {
            try {
                String userName=userInfoDto.getUserName();
                UserInfoVo userInfoVo = userMapper.selectByUserName(userName);
                if(null == userInfoVo || !userInfoVo.getPassword().equals(userInfoDto.getPassword())){
                    return ApiResult.buildFail(AUTH_LOGIN_PARAM.getCode(), AUTH_LOGIN_PARAM.getDesc());
                } else {
                    String tokenStr = JWTUtil.sign(userInfoVo);
                    //相当于存入token的时候,同时存入了用户的基本信息在redis里面,然后之后在redis没有过期的时候,可以直接去redis里面拿,不用解析token,也不用threadLocal。
                    //用户信息在有修改的时候要更新一次。
                    userService.addTokenToRedis(userInfoVo.getUserNum(),tokenStr);
                    userService.addUserInfoToRedis(userInfoVo.getUserNum(),userInfoVo);
                    return ApiResult.buildSuccessNormal("登录成功",tokenStr);
                }
            } catch (Exception e) {
                log.info("登录失败,参数:{},异常:{}",userInfoDto,e);
                return ApiResult.buildFail(AUTH_LOGIN.getCode(), AUTH_LOGIN.getDesc());
            }finally {
                //日志存储
                LogAgent.log(LogActiveProjectEnums.GEMINI,LogActiveTypeEnums.SYSTEM,userMapper.selectByUserName(userInfoDto.getUserName()).getUserNum(),LogActiveNameEnums.LOG_LOGIN,"登录");
            }
        }
    

    jwt的鉴权功能:

    @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            //获取请求头token,鉴权是否已经登录
            AuthenticationToken token = this.createToken(servletRequest, servletResponse);
            if (token.getPrincipal() == null) {
                handler401(servletResponse, AUTH_ANONYMOUS.getCode(), AUTH_ANONYMOUS.getDesc());
                return false;
            } else {
                try {
                    this.getSubject(servletRequest, servletResponse).login(token);
                    //如果token有效,那么直接获取缓存中的userInfo信息
                    String userNum = JWTUtil.getUserNum(token.getPrincipal().toString());
                    String key = CommonConstant.USER_SIMPLE_INFO + userNum;
                    UserInfoVo userInfoVo = (UserInfoVo) redisTemplate.opsForValue().get(key);
                    //用户信息塞入SessionLocal
                    SessionLocal.setUserInfo(userInfoVo);
                    return true;
                } catch (Exception e) {
                    String msg = e.getMessage();
                    //token错误
                    if (msg.contains("incorrect")) {
                        handler401(servletResponse, AUTH_ANONYMOUS.getCode(), msg);
                        return false;
                        //token过期
                    } else if (msg.contains("expired")) {
                        //尝试刷新token
                        if (this.refreshToken(servletRequest, servletResponse)) {
                            return true;
                        } else {
                            handler401(servletResponse, AUTH_ANONYMOUS.getCode(), "token已过期,请重新登录");
                            return false;
                        }
                    }
                    handler401(servletResponse, AUTH_ANONYMOUS.getCode(), msg);
                    return false;
                }
            }
        }
    //获取header中的token
        @Override
        protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String token = request.getHeader("Authorization");
            return new JWTToken(token);
        }
        //更新token
        private boolean refreshToken(ServletRequest servletRequest, ServletResponse servletResponse) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            //获取header,tokenStr
            String oldToken = request.getHeader("Authorization");
            String userNum = JWTUtil.getUserNum(oldToken);
            String key = CommonConstant.JWT_TOKEN + userNum;
            String keyU = CommonConstant.USER_SIMPLE_INFO +userNum;
            //获取redis tokenStr和缓存中的用户信息
            String redisToken = (String) redisTemplate.opsForValue().get(key);
            if (redisToken != null) {
                //如果token存在且token等于当前redis中的token,则刷新token时间
                if (oldToken.equals(redisToken)) {
                    UserInfoVo vo = this.userMapper.selectByUserNum(userNum);
                    //重写生成token(刷新)
                    String newTokenStr = JWTUtil.sign(vo);
                    JWTToken jwtToken = new JWTToken(newTokenStr);
                    userService.addTokenToRedis(userNum, newTokenStr);
                    userService.addUserInfoToRedis(userNum,vo);
                    //放进threadLocal
                    SessionLocal.setUserInfo(vo);
                    SecurityUtils.getSubject().login(jwtToken);
                    response.setHeader("Authorization", newTokenStr);
                    return true;
                }
            }
            return false;
        }
    

    在例子【更新日记】的需求中,运行到更新接口时,无需前端传无意义值或调用数据库,直接使用线程变量获取用户信息

    @Override
        public ApiResult<TextRecordVo> writeRecord(TextRecordDto textRecordDto) throws Exception {
            //获取用户信息
            UserInfoVo userInfoVo = SessionLocal.getUserInfo();
            textRecordDto.setUserNum(userInfoVo.getUserNum());
           ……
            return ApiResult.successMsg("成功创建/更新日记");
        }
    

    相关文章

      网友评论

          本文标题:全局获取用户信息

          本文链接:https://www.haomeiwen.com/subject/rvtqqctx.html