SpringBoot 并發(fā)登錄人數(shù)控制的實現(xiàn)方法
通常系統(tǒng)都會限制同一個賬號的登錄人數(shù),多人登錄要么限制后者登錄,要么踢出前者,Spring Security 提供了這樣的功能,本文講解一下在沒有使用Security的時候如何手動實現(xiàn)這個功能
demo 技術(shù)選型
- SpringBoot
- JWT
- Filter
- Redis + Redisson
JWT(token)存儲在Redis中,類似 JSessionId-Session的關(guān)系,用戶登錄后每次請求在Header中攜帶jwt
如果你是使用session的話,也完全可以借鑒本文的思路,只是代碼上需要加些改動
兩種實現(xiàn)思路
比較時間戳
維護(hù)一個 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 {
// 注銷當(dāng)前token
userService.logout(token);
sendJsonResponse(response, 4001, "您的賬號已在其他設(shè)備登錄");
return false;
}
}
return true;
}
}
隊列踢出

public class QueueKickOutFilter extends KickOutFilter {
/**
* 踢出之前登錄的/之后登錄的用戶 默認(rèn)踢出之前登錄的用戶
*/
private boolean kickoutAfter = false;
/**
* 同一個帳號最大會話數(shù) 默認(rèn)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數(shù)超出最大會話數(shù),開始踢人
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) {
// 設(shè)置會話的kickout屬性表示踢出了
kickoutSession.setKickout(true);
bucket.set(kickoutSession);
}
} catch (Exception e) {
}
}
// 如果被踢出了,直接退出,重定向到踢出后的地址
if (currentSession.isKickout()) {
// 會話被踢出了
try {
// 注銷
userService.logout(token);
sendJsonResponse(response, 4001, "您的賬號已在其他設(shè)備登錄");
} 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;
}
}
比較兩種方法
- 第一種方法邏輯簡單粗暴, 只維護(hù)一個key-value 不需要使用鎖,非要說缺點的話沒有第二種方法靈活。
- 第二種方法我很喜歡,代碼很優(yōu)雅靈活,但是邏輯相對麻煩一些,而且為了保證線程安全地操作隊列,要使用分布式鎖。目前我們項目中使用的是第一種方法
演示
下載地址: https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/login-control
- 運行項目,訪問localhost:8887 demo中沒有存儲用戶信息,隨意輸入用戶名密碼,用戶名相同則被踢出
- 訪問 localhost:8887/index.html 彈出用戶信息, 代表當(dāng)前用戶有效
- 另一個瀏覽器登錄相同用戶名,回到第一個瀏覽器刷新頁面,提示被踢出
- application.properties中選擇開啟哪種過濾器模式,默認(rèn)是比較時間戳踢出,開啟隊列踢出 queue-filter.enabled=true
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Spring Boot使用AOP在指定方法執(zhí)行完后執(zhí)行異步處理操作
- SpringBoot線程池ThreadPoolTaskExecutor異步處理百萬級數(shù)據(jù)
- springboot異步處理@NotBlank或@NotNull注釋校驗不生效問題
- Springboot?異步任務(wù)和定時任務(wù)的異步處理
- SpringBoot異步處理的四種實現(xiàn)方式
- Springboot集成定時器和多線程異步處理操作
- Springboot實現(xiàn)高吞吐量異步處理詳解(適用于高并發(fā)場景)
- SpringBoot高并發(fā)下控制限流的幾種實現(xiàn)方法
- 如何在Spring Boot中實現(xiàn)異步處理與并發(fā)控制
相關(guān)文章
springsecurity 企業(yè)微信登入的實現(xiàn)示例
本文主要介紹了springsecurity 企業(yè)微信登入的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
在IntelliJ IDEA中為自己設(shè)計的類庫生成JavaDoc的方法示例
這篇文章主要介紹了在IntelliJ IDEA中為自己設(shè)計的類庫生成JavaDoc的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
Java中Socket實現(xiàn)數(shù)據(jù)通信的示例代碼
本文主要介紹了Java中Socket實現(xiàn)數(shù)據(jù)通信的示例代碼,Socket可以建立起客戶端和服務(wù)器之間的連接,實現(xiàn)數(shù)據(jù)的傳輸和交互,感興趣的可以了解一下2023-09-09
微信開發(fā)準(zhǔn)備第一步 Maven倉庫管理新建WEB項目
這篇文章主要為大家詳細(xì)介紹了微信開發(fā)準(zhǔn)備第一步,Maven倉庫管理新建WEB項目,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
JDK1.6“新“特性Instrumentation之JavaAgent(推薦)
這篇文章主要介紹了JDK1.6“新“特性Instrumentation之JavaAgent,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08

