最近在查看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
}
],