SpringBoot 實現系統控制並發登錄人數


通常系統都會限制同一個賬號的登錄人數,多人登錄要么限制后者登錄,要么踢出前者,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;
    }
}
比較兩種方法

第一種方法邏輯簡單粗暴, 只維護一個key-value 不需要使用鎖,非要說缺點的話沒有第二種方法靈活。

第二種方法我很喜歡,代碼很優雅靈活,但是邏輯相對麻煩一些,而且為了保證線程安全地操作隊列,要使用分布式鎖。目前我們項目中使用的是第一種方法

演示

下載地址:

https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/login-control

運行項目,訪問localhost:8887 demo中沒有存儲用戶信息,隨意輸入用戶名密碼,用戶名相同則被踢出

訪問 localhost:8887/index.html 彈出用戶信息, 代表當前用戶有效

另一個瀏覽器登錄相同用戶名,回到第一個瀏覽器刷新頁面,提示被踢出

application.properties中選擇開啟哪種過濾器模式,默認是比較時間戳踢出,開啟隊列踢出 queue-filter.enabled=true

作者:殷天文

文章來源:www.jianshu.com/p/b6f5ec98d790

推薦閱讀:

SpringBoot基於數據庫的定時任務實現

Java中大量if...else語句的消除替代方案

Java8中遍歷Map的常用四種方式

掃碼關注公眾號,發送關鍵詞獲取相關資料:
  1. 發“Springboot”領取電商項目實戰源碼;

  2. 發“SpringCloud”領取學習實戰資料;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM