同一账号在同一客户端类型只能登录一次

JeecgBoot 单点登录的核心技术栈为JWT(生成唯一登录凭证)+ Redis(存储登录状态 / 管理 token),所有核心逻辑集中在LoginController类中,是 JeecgBoot 权限体系的重要组成部分。

1.核心处理方法:handleSingleSignOn(单点登录核心)

接收用户名、新 token、客户端类型三个参数,完成 “登新踢旧” 的全部逻辑,

private void handleSingleSignOn(String username, String newToken, String clientType) {
    // 步骤1:判断配置-是否开启并发登录(开启则跳过单点登录,直接返回)
    if (jeecgBaseConfig.getFirewall() == null || Boolean.TRUE.equals(jeecgBaseConfig.getFirewall().getIsConcurrent())) {
        log.debug("并发登录已启用:用户[{}]在{}端允许多地同时登录", username, clientType);
        return;
    }
    // 步骤2:根据客户端类型,匹配对应的Redis Key前缀(核心:客户端隔离)
    String redisKeyPrefix;
    if (CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)) {
        redisKeyPrefix = CommonConstant.PREFIX_USER_TOKEN_APP;
    } else if (CommonConstant.CLIENT_TYPE_PHONE.equalsIgnoreCase(clientType)) {
        redisKeyPrefix = CommonConstant.PREFIX_USER_TOKEN_PHONE;
    } else {
        redisKeyPrefix = CommonConstant.PREFIX_USER_TOKEN_PC;
    }
    String userTokenKey = redisKeyPrefix + username; // 最终Key:前缀+用户名
    
    // 步骤3:获取该用户在当前客户端的旧token
    Object oldTokenObj = redisUtil.get(userTokenKey);
    if (oldTokenObj != null && !oldTokenObj.equals(newToken)) {
        String oldToken = oldTokenObj.toString();
        // 步骤4:踢掉旧登录-清理旧token的有效缓存,设置被踢提示(1小时过期)
        redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + oldToken); // 清除旧token的核心缓存
        redisUtil.set(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + oldToken, 
                      "不允许同一账号多地同时登录,当前登录被踢掉!", 
                      60 * 60); // 错误提示缓存1小时
        log.info("用户[{}]在{}端的旧登录已被踢下线,新token:{},旧token:{}", username, clientType, newToken, oldToken);
    }
    
    // 步骤5:保存新token-将新token存入Redis,设置与JWT一致的过期时间
    redisUtil.set(userTokenKey, newToken);
    redisUtil.expire(userTokenKey, JwtUtil.EXPIRE_TIME * 2 / 1000);
}

核心要点:

  • 配置开关isConcurrent为true时,直接跳过单点登录逻辑,允许多地登录;
  • 客户端隔离的核心是不同客户端使用不同的 Redis Key 前缀,确保登录状态互不干扰;
  • 旧 token 清理后,通过PREFIX_USER_TOKEN_ERROR_MSG + oldToken存储错误提示,供前端 / 接口校验时读取。

2.Token 生成与初始化:userInfo方法(登录成功后调用)

单点登录的前提是生成有效的 JWT token,并完成基础缓存初始化,该逻辑在userInfo方法中实现,核心步骤:

  1. 生成 JWT token:通过JwtUtil.sign(username, syspassword, clientType)生成含客户端类型的 token;
  2. 初始化 token 核心缓存:将 token 存入PREFIX_USER_TOKEN + token,并设置过期时间(PC 端为 JWT 过期时间的 2 倍,APP 端为 APP_JWT 过期时间的 2 倍);
  3. 调用单点登录方法:执行handleSingleSignOn(username, token, clientType),完成登新踢旧;
  4. 后续处理:设置用户信息、部门信息、字典数据等,返回给前端。
// 生成token,并设置超时时间
String token = JwtUtil.sign(username, syspassword, clientType);
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
// 根据客户端类型设置过期时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
obj.put("token", token);
// 执行单点登录逻辑-登新踢旧
handleSingleSignOn(username, token, clientType);

3.缓存清理:asyncClearLogoutCache方法(退出登录时异步执行)

用户主动退出登录时,需要异步清理所有相关缓存,避免阻塞登录请求,该方法通过自定义线程池cachedThreadPool实现异步执行,核心清理内容:

  1. 清理 token 核心缓存:PREFIX_USER_TOKEN + token;
  2. 清理 Shiro 权限缓存:PREFIX_USER_SHIRO_CACHE + 用户ID;
  3. 清理用户基础信息缓存:CacheConstant.SYS_USERS_CACHE + 用户名;
  4. 清理单点登录的 token 标识:PREFIX_USER_TOKEN_PC/APP/PHONE + 用户名;
  5. 记录退出日志。
private void asyncClearLogoutCache(String token, LoginUser sysUser) {
    cachedThreadPool.execute(()->{
    redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token); // 清理核心token缓存
    redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId()); // 清理Shiro缓存
    redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername())); // 清理用户信息缓存
    // 清理单点登录的token标识(全客户端)
    redisUtil.del(CommonConstant.PREFIX_USER_TOKEN_PC + sysUser.getUsername());
    redisUtil.del(CommonConstant.PREFIX_USER_TOKEN_APP + sysUser.getUsername());
    redisUtil.del(CommonConstant.PREFIX_USER_TOKEN_PHONE + sysUser.getUsername());
    // 记录退出日志
    baseCommonService.addLog("用户名: "+sysUser.getRealname()+",退出成功!", CommonConstant.LOG_TYPE_1, null, sysUser);
    });
}

完整单点登录执行流程

步骤 1:用户登录校验

用户提交用户名、密码、验证码,系统完成登录失败次数限制、验证码校验、用户有效性校验、密码校验(均通过后进入后续流程)。

步骤 2:生成 JWT token

调用JwtUtil.sign生成含用户名、密码、客户端类型(PC)的 token,并将 token 存入 Redis 核心缓存(sys:token:${token}),设置过期时间。

步骤 3:执行单点登录逻辑

调用handleSingleSignOn(username, newToken, CLIENT_TYPE_PC),首先判断配置文件的isConcurrent是否为false(开启单点登录)。

步骤 4:匹配 Redis Key 前缀

根据 PC 端匹配前缀PREFIX_USER_TOKEN_PC,生成最终 Key:sys:token:pc:用户名。

步骤 5:获取旧 token 并判断

从 Redis 中获取该 Key 对应的旧 token,若旧 token 存在且与新 token 不一致,则执行 “踢旧” 逻辑;若旧 token 不存在 / 与新 token 一致,直接保存新 token。

步骤 6:踢掉旧登录

  1. 清理旧 token 的核心缓存:redisUtil.del(sys:token:${oldToken});
  2. 为旧 token 设置被踢提示:sys:token:error:${oldToken},存储 “不允许同一账号多地同时登录,当前登录被踢掉!”,有效期 1 小时;
  3. 打印日志,记录 “旧登录被踢下线” 的信息。

步骤 7:保存新 token 的单点标识

将新 token 存入sys:token:pc:用户名,设置与 JWT 一致的过期时间,作为该用户 PC 端的最新登录标识。

步骤 8:登录成功后续处理

  1. 清理该用户的登录失败次数缓存;
  2. 记录用户登录成功日志;
  3. 将 token、用户信息、部门信息等返回给前端。

旧登录被踢后的后续影响

旧登录的 token 因核心缓存被清理,当用户使用旧 token 访问系统接口时,接口会校验sys:token:${oldToken}是否存在:

  • 若不存在,直接返回 “token 无效”;
  • 若存在sys:token:error:${oldToken},则返回对应的被踢下线提示,前端接收到后跳转到登录页。
打开下面链接,直接免费下载资源: https://renmaiwang.cn/s/jamfn 在IT行业中,要求“同一账号同一时间只能允许他登录一次”这一需求通常被称为单点登录(Single Sign-On, SSO)。这是一个确保系统安全性的重要措施,旨在防止账户被多设备或不地点时使用。通过session机制即可实现这一目标。单点登录的核心思想是:用户完成首次登录后,系统将生成一个唯一的session ID并存储在服务器,当用户访问新的页面时,会携带该session ID进行验证。如果发现同一账号在另一处试图登录,服务器将检查现有session,并若发现该账号已在线则拒绝新请求。具体实现步骤如下:1. 用户首次登录时,服务器需验证其用户名和密码。成功后创建新的session对象并存储用户信息(如用户ID),通常使用$request.getSession().setAttribute("userId", userId);来操作。2. 在每次访问新页面时,server会携带session ID进行验证。这一过程在filter中完成以确保请求合法性。3. filter需检查新请求的session是否与已登录的session匹配。若匹配则表示用户已在线,此时可依据业务需求采取相应措施,例如提示用户已被其他地方登录或注销前登录,并使用$session.invalidate();终止旧会话。4. 若未找到匹配session,则允许访问并更新session信息。5. 在多线程环境中需注意session的并发控制以避免数据冲突。6. 为防止session hijacking可在session中加入IP地址、User-Agent等安全信息,确保 session 复制无法成功登录。7. 时应合理设置session超时时间以避免无效session占用内存。8. 对于大型分布式系统可能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值