通常系统都会限制同一个账号的登录人数,多人登录要么限制后者登录,要么踢出前者,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. 【强化学习】DQN 算法改进

    DQN 算法改进 (一)Dueling DQN Dueling DQN 是一种基于 DQN 的改进算法.主要突破点:利用模型结构将值函数表示成更加细致的形式,这使得模型能够拥有更好的表现.下面给出公式 ...

  2. AntV G2 图表tooltip重命名

    在做数据可视化的过程中,遇到了一个问题,就是给图表tooltip重命名. 在研究后,发现了三种方法: 方法1:一般情况下,可以在给chart绑定数据源时,添加scale配置,并在scale配置中设置别 ...

  3. git 使用详解 (1)——历史

    版本控制系统(VCS) 有了它你就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态.你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而导致出现怪异问题,又是谁在何时报 ...

  4. JavaScript2 基础

    运算符 赋值运算符  用于给变量赋值. y=5;/z=2; 算术运算符  即算数符号,是基本算数运算.+ 加 / - 减/ * 乘/ / 除/ % 取余数/ ++ 自增(y++先赋值再自增/++y先自 ...

  5. [TimLinux] JavaScript 取消异步请求

    1. xhr.abort() 这个函数可以用来取消XMLHttpRequest()发起的异步请求,不是xhr.close()哦. 2. 场景 比如说,在关闭一个模态框后,数据可能还没有过来,这个时候需 ...

  6. axios报错: Cannot read property 'protocol' of undefined ....

    错误: Uncaught (in promise) TypeError: **Cannot read property 'protocol' of undefined ... 源码: 完整错误: im ...

  7. ACM-ICPC 2018 焦作赛区网络预赛 L 题 Poor God Water

    God Water likes to eat meat, fish and chocolate very much, but unfortunately, the doctor tells him t ...

  8. 使用g++编译器扩大程序可用栈空间

    如题,在写一些程序的时候我们有时会开一个比较大的数组或进行层数较多的dfs.这时候,程序常常会报错,于是就很无奈. 其实,虽然Windows给程序的默认栈空间比较小,我们还是有办法去扩大这个程序运行栈 ...

  9. 笔记||Python3之面向对象

    面向对象编程:简称OOP. 是一种程序设计思想.oop把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数. 在python中,所有数据类型都可以视为对象,也可以自定义对象.自定义的对象数据类 ...

  10. CMFCPropertyGridCtrl的属性改变事件代码

    //用于区分Prop, 使用SetData, GetData方法 CMFCPropertyGridProperty* pProp1 = new CMFCPropertyGridProperty(str ...