通常系统都会限制同一个账号的登录人数,多人登录要么限制后者登录,要么踢出前者,Spring Security 提供了这样的功能,本文讲解一下在没有使用Security的时候如何手动实现这个功能

demo 技术选型

  • SpringBoot
  • JWT
  • Filter
  • Redis + Redisson

JWT(token)存储在Redis中,类似 JSessionId-Session的关系,用户登录后每次请求在Header中携带jwt

如果你是使用session的话,也完全可以借鉴本文的思路,只是代码上需要加些改动

两种实现思路

比较时间戳

维护一个 username: jwtToken 这样的一个 key-value 在Reids中, Filter逻辑如下

public class CompareKickOutFilter extends KickOutFilter {

    @Autowired
private UserService userService; @Override
public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader("Authorization");
String username = JWTUtil.getUsername(token);
String userKey = PREFIX + username; RBucket<String> bucket = redissonClient.getBucket(userKey);
String redisToken = bucket.get(); if (token.equals(redisToken)) {
return true; } else if (StringUtils.isBlank(redisToken)) {
bucket.set(token); } else {
Long redisTokenUnixTime = JWTUtil.getClaim(redisToken, "createTime").asLong();
Long tokenUnixTime = JWTUtil.getClaim(token, "createTime").asLong(); // token > redisToken 则覆盖
if (tokenUnixTime.compareTo(redisTokenUnixTime) > 0) {
bucket.set(token); } else {
// 注销当前token
userService.logout(token);
sendJsonResponse(response, 4001, "您的账号已在其他设备登录");
return false; } } return true; }
}

队列踢出

public class QueueKickOutFilter extends KickOutFilter {
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
*/
private boolean kickoutAfter = false;
/**
* 同一个帐号最大会话数 默认1
*/
private int maxSession = 1; public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
} public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
} @Override
public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) throws Exception {
String token = request.getHeader("Authorization");
UserBO currentSession = CurrentUser.get();
Assert.notNull(currentSession, "currentSession cannot null");
String username = currentSession.getUsername();
String userKey = PREFIX + "deque_" + username;
String lockKey = PREFIX_LOCK + username; RLock lock = redissonClient.getLock(lockKey); lock.lock(2, TimeUnit.SECONDS); try {
RDeque<String> deque = redissonClient.getDeque(userKey); // 如果队列里没有此token,且用户没有被踢出;放入队列
if (!deque.contains(token) && currentSession.isKickout() == false) {
deque.push(token);
} // 如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
String kickoutSessionId;
if (kickoutAfter) { // 如果踢出后者
kickoutSessionId = deque.removeFirst();
} else { // 否则踢出前者
kickoutSessionId = deque.removeLast();
} try {
RBucket<UserBO> bucket = redissonClient.getBucket(kickoutSessionId);
UserBO kickoutSession = bucket.get(); if (kickoutSession != null) {
// 设置会话的kickout属性表示踢出了
kickoutSession.setKickout(true);
bucket.set(kickoutSession);
} } catch (Exception e) {
} } // 如果被踢出了,直接退出,重定向到踢出后的地址
if (currentSession.isKickout()) {
// 会话被踢出了
try {
// 注销
userService.logout(token);
sendJsonResponse(response, 4001, "您的账号已在其他设备登录"); } catch (Exception e) {
} return false; } } finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
LOGGER.info(Thread.currentThread().getName() + " unlock"); } else {
LOGGER.info(Thread.currentThread().getName() + " already automatically release lock");
}
} return true;
} }

比较两种方法

  1. 第一种方法逻辑简单粗暴, 只维护一个key-value 不需要使用锁,非要说缺点的话没有第二种方法灵活。
  2. 第二种方法我很喜欢,代码很优雅灵活,但是逻辑相对麻烦一些,而且为了保证线程安全地操作队列,要使用分布式锁。目前我们项目中使用的是第一种方法

本人免费整理了Java高级资料,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G,需要自己领取。
传送门:https://mp.weixin.qq.com/s/osB-BOl6W-ZLTSttTkqMPQ

SpringBoot 并发登录人数控制的更多相关文章

  1. 2017.4.12 开涛shiro教程-第十八章-并发登录人数控制

    原博客地址:http://jinnianshilongnian.iteye.com/blog/2018398 根据下载的pdf学习. 开涛shiro教程-第十八章-并发登录人数控制 shiro中没有提 ...

  2. 第十八章 并发登录人数控制——《跟我学Shiro》

    目录贴:跟我学Shiro目录贴 在某些项目中可能会遇到如每个账户同时只能有一个人登录或几个人同时登录,如果同时有多人登录:要么不让后者登录:要么踢出前者登录(强制退出).比如spring securi ...

  3. 2017.6.30 用shiro实现并发登录人数控制(实际项目中的实现)

    之前的学习总结:http://www.cnblogs.com/lyh421/p/6698871.html 1.kickout功能描述 如果将配置文件中的kickout设置为true,则在另处再次登录时 ...

  4. 并发登录人数控制--Shiro系列(二)

    为了安全起见,同一个账号理应同时只能在一台设备上登录,后面登录的踢出前面登录的.用Shiro可以轻松实现此功能. shiro中sessionManager是专门作会话管理的,而sessinManage ...

  5. Windows登录脚本可以限制并发登录吗

    在Windows服务器中,使用一个Windows登录脚本来限制并发会话靠谱吗? 事实上,这种解决方案存在很多缺点和弱点,并不能满足大中型IT基础设施的安全性需求. 一.使用登陆脚本限制并发会话,恶意用 ...

  6. JAVA之旅(三十三)——TCP传输,互相(伤害)传输,复制文件,上传图片,多并发上传,多并发登录

    JAVA之旅(三十三)--TCP传输,互相(伤害)传输,复制文件,上传图片,多并发上传,多并发登录 我们继续网络编程 一.TCP 说完UDP,我们就来说下我们应该重点掌握的TCP了 TCP传输 Soc ...

  7. SpringBoot注册登录(三):注册--验证账号密码是否符合格式及后台完成注册功能

    SpringBoot注册登录(一):User表的设计点击打开链接SpringBoot注册登录(二):注册---验证码kaptcha的实现点击打开链接      SpringBoot注册登录(三):注册 ...

  8. 063 日志分析(pv  uv  登录人数  游客人数  平均访问时间  二跳率  独立IP)

    1.需求分析 分析指标 pv uv 登录人数 游客人数 平均访问时间 二跳率 独立IP 2.使用的日志(一号店),会话信息 3.创建数据库 4.创建源表,存储源数据 5.创建我们需要的use表 6.创 ...

  9. springboot之登录注册

    springboot之登录注册 目录结构 pom.xml <?xml version="1.0" encoding="UTF-8"?> <pr ...

随机推荐

  1. Forrester:华为云容器是容器混合云最佳选择

    近日,国际权威咨询机构Forrester发布<The Forrester New WaveTM: Public Cloud Enterprise Container Platforms, Q3 ...

  2. docker的安装及常用命令

    一:概述 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器或Windows 机器上,也可以实现虚拟化,容器是完全使用 ...

  3. 初探hook的键盘获取

    初探hook的键盘获取 import pyHook import pythoncom class e(): keyIsPressed = False #键盘是否按下 按住.. def onKeyDow ...

  4. PlantUML Viewer Chrome 插件 画时序图

    PlantUML通过简单直观的语言来定义示意图 使用 Chrome+ PlantUML Viewer的插件画图 1,打开chrome网上应用店 2,搜索plantuml viewer 并添加 3,扩展 ...

  5. asp.net core 配置微信返回信息接口

    这玩意弄了两天 微信的API需要升级了 只有一份PHP的DEMO 还有个坑 返回值一定要是 微信传入的echostr 否则 出现配置失败,这个问题耗了我俩小时 最后还是在论坛看到的解决方案. 项目必须 ...

  6. bash单引号嵌套

    转自:https://blog.jysoftware.com/2015/12/bash-%E6%80%8E%E4%B9%88%E5%81%9A%E5%8D%95%E5%BC%95%E5%8F%B7%E ...

  7. WebAPI接口的自动化测试1

    自动化测试要满足四个条件: 1 - 自动化用例能够完成所有测试步骤   -------- postman不支持  没有完整的用例管理系统 2 - 每个用例的输入数据,必须要自动填入  -------- ...

  8. 这几种JavaScript语法不要轻易使用,容易出事

    文章目录 12种不宜使用的JavaScript语法 1. == 2. with 3. eval 4. continue 5. switch 贯穿 6. 单行的块结构 7. ++和-- 8. 位运算符 ...

  9. python错误处理—try…catch…finally、调用栈分析

    高级语言包括python一般都内置了一套try…catch…finally的错误处理机制: >>> try: ... print('try...') ... r = 10 / 0 . ...

  10. WinForm自定义控件之DefaultValue的误解

    DefaultValue,顾名思义,默认值的意思.但这个默认值不是用来显示的,它的作用是当属性设置的值(无法代码写还是属性窗口输入)与DefaultValue相同时,会区别显示,比如其它值加粗,Def ...