全局获取用户信息

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

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

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

况且更新当前用户信息的时候其实也是不能让前端传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("成功创建/更新日记");
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353