SpringBoot 實現併發登陸人數控制

Python實戰社羣php

Java實戰社羣html

長按識別下方二維碼,按需求添加git

掃碼關注添加客服程序員

進Python社羣▲web

掃碼關注添加客服redis

進Java社羣spring

做者丨殷天文瀏覽器

www.jianshu.com/p/b6f5ec98d790安全

今天跟你們分享SpringBoot 實現併發登陸人數控制的知識。
springboot

1 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


本文借鑑了
https://jinnianshilongnian.iteye.com/blog/2039760

程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣

近期精彩內容推薦:   再見!深圳!再見!騰訊! 瘋傳朋友圈的 Pony 馬化騰的講話 SpringBoot 實現併發登陸人數控制 異步 Python 比同步 Python 快在哪裏?



在看點這裏好文分享給更多人↓↓
相關文章
相關標籤/搜索