功能开发 #I1U09M 设置一个账号不能两个人同时登陆,后台实现

This commit is contained in:
dingxl 2020-09-12 21:56:04 +08:00
parent 790f4177d1
commit fa2a6cd6a3
8 changed files with 248 additions and 12 deletions

View File

@ -77,7 +77,8 @@ public interface CommonConstant {
public static final String PREFIX_USER_TOKEN = "prefix_user_token_";
/** Token缓存时间3600秒即一小时 */
public static final int TOKEN_EXPIRE_TIME = 3600;
/** 登录用户当前已经登录token支持不允许多终端同时登录 */
public static final String PREFIX_CURRENT_TOKEN = "prefix_current_token_";
/**
* 0一级菜单

View File

@ -116,4 +116,8 @@ public class LoginUser {
/**设备id uniapp推送用*/
private String clientId;
/**
* 是否允许多终端同时登录0或空无限制1不允许多客户端同时登陆
*/
private Integer soloLogin;
}

View File

@ -32,6 +32,21 @@ public class TokenUtils {
return token;
}
/**
* 缓存登录token同时记录用户和token的关联关系
*/
public static void cacheToken(RedisUtil redisUtil, String token, String newToken, String userId) {
// 设置token缓存有效时间
String key = CommonConstant.PREFIX_USER_TOKEN + token;
redisUtil.set(key, newToken);
redisUtil.expire(key, JwtUtil.EXPIRE_TIME * 2 / 1000);
// 登录用户当前已经登录token支持不允许多终端同时登录
key = CommonConstant.PREFIX_CURRENT_TOKEN + userId;
redisUtil.set(key, token);
redisUtil.expire(key, JwtUtil.EXPIRE_TIME * 2 / 1000);
}
/**
* 验证Token
*/
@ -124,5 +139,4 @@ public class TokenUtils {
}
return true;
}
}

View File

@ -13,6 +13,7 @@ import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.modules.cas.util.CASServiceUtil;
import org.jeecg.modules.cas.util.XmlUtils;
import org.jeecg.modules.system.entity.SysDepart;
@ -83,9 +84,8 @@ public class CasClientController {
return result;
}
String token = JwtUtil.sign(sysUser.getUsername(), sysUser.getPassword());
// 设置超时时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME*2 / 1000);
// 缓存token设置超时时间
TokenUtils.cacheToken(redisUtil, token, token, sysUser.getId());
//update-begin-author:taoyan date:20200812 for:登录缓存用户信息
LoginUser vo = new LoginUser();

View File

@ -0,0 +1,191 @@
package org.jeecg.modules.shiro.authc;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.ISysUserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* @Description: 用户登录鉴权和获取用户授权
* @Author: Scott
* @Date: 2019-4-23 8:13
* @Version: 1.1
*/
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Autowired
@Lazy
private ISysUserService sysUserService;
@Autowired
@Lazy
private ISysBaseAPI sysBaseAPI;
@Autowired
@Lazy
private RedisUtil redisUtil;
/**
* 必须重写此方法不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
* 触发检测用户权限时才会调用此方法例如checkRole,checkPermission
*
* @param principals 身份信息
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("===============Shiro权限认证开始============ [ roles、permissions]==========");
String username = null;
if (principals != null) {
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
username = sysUser.getUsername();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置用户拥有的角色集合比如admin,test
Set<String> roleSet = sysUserService.getUserRolesSet(username);
info.setRoles(roleSet);
// 设置用户拥有的权限集合比如sys:role:add,sys:user:add
Set<String> permissionSet = sysUserService.getUserPermissionsSet(username);
info.addStringPermissions(permissionSet);
log.info("===============Shiro权限认证成功==============");
return info;
}
/**
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
* 也就是说验证用户输入的账号和密码是否正确错误抛出异常
*
* @param auth 用户登录的账号密码信息
* @return 返回封装了用户信息的 AuthenticationInfo 实例
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
if (token == null) {
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest()));
throw new AuthenticationException("token为空!");
}
// 校验token有效性
LoginUser loginUser = this.checkUserTokenIsEffect(token);
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
/**
* 校验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 = sysBaseAPI.getUserByName(username);
if (loginUser == null) {
throw new AuthenticationException("用户不存在!");
}
// 判断用户状态
if (loginUser.getStatus() != 1) {
throw new AuthenticationException("账号已被锁定,请联系管理员!");
}
// 如果用户不允许多终端同时登录验证token是否已被清除
if (loginUser.getSoloLogin() != null && loginUser.getSoloLogin() == 1) {
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
if (oConvertUtils.isEmpty(cacheToken)) {
log.info("多客户端登录: " + loginUser.getId() + "无效token" + token);
throw new AuthenticationException("账号不允许多个客户端同时登录,请联系管理员!");
}
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, loginUser.getId(), username, loginUser.getPassword())) {
throw new AuthenticationException("Token失效请重新登录!");
}
return loginUser;
}
/**
* JWTToken刷新生命周期 实现 用户在线操作不掉线功能
* 1登录成功后将用户的JWT生成的Token作为kv存储到cache缓存里面(这时候kv值一样)缓存有效期设置为Jwt有效时间的2倍
* 2当该用户再次请求时通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
* 3当该用户这次请求jwt生成的token值已经超时但该token对应cache中的k还是存在则表示该用户一直在操作只是JWT的token失效了程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值该缓存生命周期重新计算
* 4当该用户这次请求jwt在生成的token值已经超时并在cache中不存在对应的k则表示该用户账户空闲超时返回用户信息已失效请重新登录
* 注意 前端请求Header中设置Authorization保持不变校验有效性以缓存中的token为准
* 用户过期时间 = Jwt有效时间 * 2
*
* @param userName
* @param passWord
* @return
*/
public boolean jwtTokenRefresh(String token, String userId, String userName, String passWord) {
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
String newAuthorization = JwtUtil.sign(userName, passWord);
// 缓存token设置超时时间
TokenUtils.cacheToken(redisUtil, token, newAuthorization, userId);
log.info("——————————用户在线操作更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
}
//update-begin--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
// else {
// // 设置超时时间
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
// }
//update-end--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
return true;
}
return false;
}
/**
* 清除当前用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
}

View File

@ -105,7 +105,17 @@ public class LoginController {
result.error500("用户名或密码错误");
return result;
}
//3. 如果用户不允许多终端同时登录
if (sysUser.getSoloLogin() != null && sysUser.getSoloLogin() == 1) {
String token = String.valueOf(redisUtil.get(CommonConstant.PREFIX_CURRENT_TOKEN + sysUser.getId()));
// 删除已有其他客户端登录Token缓存
if (!oConvertUtils.isEmpty(token)) {
redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
log.info("多客户端登录: " + sysUser.getId() + "将已有token失效" + token);
}
}
//用户登录信息
userInfo(sysUser, result);
//update-begin--Author:wangshuai Date:20200714 for登录日志没有记录人员
@ -356,9 +366,9 @@ public class LoginController {
String username = sysUser.getUsername();
// 生成token
String token = JwtUtil.sign(username, syspassword);
// 设置token缓存有效时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME*2 / 1000);
// 缓存token设置超时时间
TokenUtils.cacheToken(redisUtil, token, token, sysUser.getId());
log.info("多客户端登录: " + sysUser.getId() + "token" + token);
//update-begin-author:taoyan date:20200812 for:登录缓存用户信息
LoginUser vo = new LoginUser();
@ -459,6 +469,16 @@ public class LoginController {
result.error500("用户名或密码错误");
return result;
}
//3. 如果用户不允许多终端同时登录
if (sysUser.getSoloLogin() != null && sysUser.getSoloLogin() == 1) {
String token = String.valueOf(redisUtil.get(CommonConstant.PREFIX_CURRENT_TOKEN + sysUser.getId()));
// 删除已有其他客户端登录Token缓存
if (!oConvertUtils.isEmpty(token)) {
redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
log.info("多客户端登录: " + sysUser.getId() + "将已有token失效" + token);
}
}
String orgCode = sysUser.getOrgCode();
if(oConvertUtils.isEmpty(orgCode)) {
@ -478,9 +498,8 @@ public class LoginController {
// 生成token
String token = JwtUtil.sign(username, syspassword);
// 设置超时时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME*2 / 1000);
// 缓存token设置超时时间
TokenUtils.cacheToken(redisUtil, token, token, sysUser.getId());
//update-begin-author:taoyan date:20200812 for:登录缓存用户信息
LoginUser vo = new LoginUser();

View File

@ -195,4 +195,9 @@ public class SysUser implements Serializable {
/**设备id uniapp推送用*/
private String clientId;
/**
* 是否允许多终端同时登录0或空无限制1不允许多客户端同时登陆
*/
private Integer soloLogin;
}

View File

@ -0,0 +1,2 @@
ALTER TABLE `sys_user`
ADD COLUMN `solo_login` tinyint(1) NULL DEFAULT NULL COMMENT '是否允许多终端同时登录0或空无限制1不允许多客户端同时登陆';