jeecg boot 3.2.0 用户token刷新在线用户显示问题

最近在查看jeecg 中在线用户的时候发现过一段时间在线用户中admin(我)不见了

原因查找

观察前端关于判断在线用户是我的代码如下:

javascript 复制代码
let currentToken = this.$ls.get(ACCESS_TOKEN)
      return {
        description: '在线用户管理页面',
        queryParam: {
          username: ''
        },
        // 表头
        columns: [
          {
            title:'用户账号',
            align:"center",
            dataIndex: 'username',
            customRender: (text,record) => {
              // 比较键因为值会被刷新掉,所以不用token
              if(record.token === currentToken) {
                return text + '(我)'
              }
              return text
            },
          },{
            title:'用户姓名',
            align:"center",
            dataIndex: 'realname'
          },{
            title: '头像',
            align: "center",
            width: 120,
            dataIndex: 'avatar',
            scopedSlots: {customRender: "avatarslot"}
          },{
            title:'生日',
            align:"center",
            dataIndex: 'birthday'
          },{
            title: '性别',
            align: "center",
            dataIndex: 'sex',
            customRender: (text) => {
              //字典值翻译通用方法
              return filterDictTextByCache('sex', text);
            }
          },{
            title:'手机号',
            align:"center",
            dataIndex: 'phone'
          },{
            title:'过期时间',
            align: "center",
            dataIndex:'expireTime'
          },{
            title: '操作',
            dataIndex: 'action',
            scopedSlots: {customRender: 'action'},
            align: "center",
            width: 170
          }
        ],

可以确定前端来判断这个用户是不是我是通过接口中返回的token去判断的,但是过一段时间会没有是因为,token在调用接口的时候进行刷新了

后端刷新代码如下:

java 复制代码
  @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
        String token = (String) auth.getCredentials();
        if (token == null) {
            HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
            log.info("------------------------身份认证失败------------------------------IP地址:  {},URL:{}", oConvertUtils.getIpAddrByRequest(req), req.getRequestURI());
            throw new AuthenticationException("token为空!");
        }
        // 校验token有效性
        LoginUser loginUser = null;
        try {
            // 这里会刷新token的值导致前端保存的值和实际的值不一致
            loginUser = this.checkUserTokenIsEffect(token);
        } catch (AuthenticationException e) {
            JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
            log.error("身份认证异常:", e);
            return null;
        }
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }
java 复制代码
/**
     * 校验token的有效性
     *
     * @param token
     */
    public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解密获得username,用于和数据库进行对比
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }

        // 查询用户信息
        log.debug("---------校验token是否有效------------checkUserTokenIsEffect--------------------- "+ token);
        LoginUser loginUser = TokenUtils.getLoginUser(username,commonApi,redisUtil);
        //LoginUser loginUser = commonApi.getUserByName(username);
        if (loginUser == null) {
            throw new AuthenticationException("用户不存在!");
        }
        // 判断用户状态
        if (loginUser.getStatus() != 1) {
            throw new AuthenticationException("账号已被锁定,请联系管理员!");
        }
        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
            throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
        }
        //update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
        String userTenantIds = loginUser.getRelTenantIds();
        if(oConvertUtils.isNotEmpty(userTenantIds)){
            String contextTenantId = TenantContext.getTenant();
            String str ="0";
            if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
                //update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
                String[] arr = userTenantIds.split(",");
                if(!oConvertUtils.isIn(contextTenantId, arr)){
                    throw new AuthenticationException("用户租户信息变更,请重新登陆!");
                }
                //update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
            }
        }
        //update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
        return loginUser;
    }

问题的关键就在于jwtTokenRefresh这个方法里面对用户在线情况的判断

代码中只是判断了reids是否有这个缓存的键名,但是更新的又是键值,返回的token也是键值,在键值发生变化的时候,那个"(我)"也就消失了

java 复制代码
/**
     * 刷新token(保证用户在线操作不掉线)
     * @param token
     * @param userName
     * @param passWord
     * @param redisUtil
     * @return
     */
    private static boolean jwtTokenRefresh(String token, String userName, String passWord, RedisUtil redisUtil) {
         // 这里判断的只是redis缓存中有没有这个键的缓存,所以一但刷新之后键名是不变的但是键值发生了变化,后端的token校验也是通过键名获取
        String cacheToken = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (oConvertUtils.isNotEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                String newAuthorization = JwtUtil.sign(userName, passWord);
                // 设置Toekn缓存有效时间
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
            }
            return true;
        }
        return false;
    }
解决方案

1.第一种可以将更新后的token返回给前端通过通过reponse加载doGetAuthenticationInfo方法里面,前端去对应的去这个请求头,

java 复制代码
 try {
            // 这里会刷新token的值导致前端保存的值和实际的值不一致
            loginUser = this.checkUserTokenIsEffect(token);
            HttpServletResponse response = SpringContextUtils.getHttpServletResponse();
            response.setHeader("X-REFRESH-TOKEN", redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token).toString());
            response.setHeader("Access-Control-Expose-Headers", "X-REFRESH-TOKEN");
        } catch (AuthenticationException e) {
            JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
            log.error("身份认证异常:", e);
            return null;
        }

2.第二种可以使用websocket将最新的token推送到前端,实现是来相对复杂(不推荐)除非是对实时性要求比较高的项目
3.第三种也就是博主投机取巧的一个办法在,在在线用户list接口中增加返回redis存储的对应的键名,实体类也对应增加一个tokenKey,代码如下

java 复制代码
@RequestMapping(value = "/list", method = RequestMethod.GET)
    public Result<Page<SysUserOnlineVO>> list(@RequestParam(name="username", required=false) String username,
                                              @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,@RequestParam(name="pageSize", defaultValue="10") Integer pageSize
    , HttpServletRequest res) {
        Collection<String> keys = redisTemplate.keys(CommonConstant.PREFIX_USER_TOKEN + "*");
        List<SysUserOnlineVO> onlineList = new ArrayList<SysUserOnlineVO>();
        LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        for (String key : keys) {
            Date expire = new Date(Instant.now().toEpochMilli()+ redisUtil.getExpire(key)*1000);
            String token = (String)redisUtil.get(key);
            if (StringUtils.isNotEmpty(token)) {
                SysUserOnlineVO online = new SysUserOnlineVO();
                online.setToken(token);
                // 增加一个对应键名的返回
                online.setTokenKey(key);
                online.setExpireTime(expire);
                //TODO 改成一次性查询
                LoginUser loginUser = sysBaseApi.getUserByName(JwtUtil.getUsername(token));
                if (loginUser != null) {
                    //update-begin---author:wangshuai ---date:20220104  for:[JTC-382]在线用户查询无效------------
                    //验证用户名是否与传过来的用户名相同
                    boolean isMatchUsername=true;
                    //判断用户名是否为空,并且当前循环的用户不包含传过来的用户名,那么就设成false
                    if(oConvertUtils.isNotEmpty(username) && !loginUser.getUsername().contains(username)){
                        isMatchUsername = false;
                    }
                    //需要通过租户id去区分
                    if(!sysUser.getUsername().equals("admin") && !loginUser.getTenantId().contains(res.getHeader("tenant-id"))){
                        isMatchUsername = false;
                    }
                    if(isMatchUsername){
                        BeanUtils.copyProperties(loginUser, online);
                        onlineList.add(online);
                    }
                    //update-end---author:wangshuai ---date:20220104  for:[JTC-382]在线用户查询无效------------
                }
            }
        }
        Collections.reverse(onlineList);

        Page<SysUserOnlineVO> page = new Page<SysUserOnlineVO>(pageNo, pageSize);
        int count = onlineList.size();
        List<SysUserOnlineVO> pages = new ArrayList<>();
        // 计算当前页第一条数据的下标
        int currId = pageNo > 1 ? (pageNo - 1) * pageSize : 0;
        for (int i = 0; i < pageSize && i < count - currId; i++) {
            pages.add(onlineList.get(currId + i));
        }
        page.setSize(pageSize);
        page.setCurrent(pageNo);
        page.setTotal(count);
        // 计算分页总页数
        page.setPages(count % 10 == 0 ? count / 10 : count / 10 + 1);
        page.setRecords(pages);

        Result<Page<SysUserOnlineVO>> result = new Result<Page<SysUserOnlineVO>>();
        result.setSuccess(true);
        result.setResult(page);
        return result;
    }

修改后前端比较的代码也修改为如下代码即可:键名前缀根据你代码里面固定的前缀来拼接

javascript 复制代码
let currentToken = 'prefix_user_token_'+this.$ls.get(ACCESS_TOKEN)
      return {
        description: '在线用户管理页面',
        queryParam: {
          username: ''
        },
        // 表头
        columns: [
          {
            title:'用户账号',
            align:"center",
            dataIndex: 'username',
            customRender: (text,record) => {
              // 比较键因为值会被刷新掉,所以不用token
              if(record.tokenKey === currentToken) {
                return text + '(我)'
              }
              return text
            },
          },{
            title:'用户姓名',
            align:"center",
            dataIndex: 'realname'
          },{
            title: '头像',
            align: "center",
            width: 120,
            dataIndex: 'avatar',
            scopedSlots: {customRender: "avatarslot"}
          },{
            title:'生日',
            align:"center",
            dataIndex: 'birthday'
          },{
            title: '性别',
            align: "center",
            dataIndex: 'sex',
            customRender: (text) => {
              //字典值翻译通用方法
              return filterDictTextByCache('sex', text);
            }
          },{
            title:'手机号',
            align:"center",
            dataIndex: 'phone'
          },{
            title:'过期时间',
            align: "center",
            dataIndex:'expireTime'
          },{
            title: '操作',
            dataIndex: 'action',
            scopedSlots: {customRender: 'action'},
            align: "center",
            width: 170
          }
        ],
相关推荐
Yvonne爱编码3 天前
前端工程化进阶:从搭建完整项目脚手架到性能优化【技术类】
前端·状态模式
Mr_sun.3 天前
Day02——基础数据开发-服务管理前端
前端·状态模式
xiaoxue..3 天前
Zustand 状态管理:轻量高效的 React 状态解决方案✨
前端·react.js·面试·状态模式·zustand
数据库知识分享者小北4 天前
从极速复制“死了么”APP,看AI编程时代的技术选型
数据库·阿里云·状态模式·ai编程·supabase
桃子叔叔4 天前
react-wavesurfer录音组件2:前端如何处理后端返回的仅Blob字段
前端·react.js·状态模式
牵牛老人4 天前
Qt后端开发遇到跨域问题终极解决方案 与 Nginx反向代理全解析
qt·nginx·状态模式
C_心欲无痕5 天前
有限状态机在前端中的应用
前端·状态模式
雨中散步撒哈拉5 天前
22、做中学 | 高一下期 | Golang反射
开发语言·golang·状态模式
小陈phd5 天前
langGraph从入门到精通(四)——基于LangGraph的State状态模式设计
python·microsoft·状态模式