SpringBoot 併發登陸人數控制

一般系統都會限制同一個帳號的登陸人數,多人登陸要麼限制後者登陸,要麼踢出前者,Spring Security 提供了這樣的功能,本文講解一下在沒有使用Security的時候如何手動實現這個功能redis

demo 技術選型安全

  • SpringBoot
  • JWT
  • Filter
  • Redis + Redisson

JWT(token)存儲在Redis中,相似 JSessionId-Session的關係,用戶登陸後每次請求在Header中攜帶jwtsession

若是你是使用session的話,也徹底能夠借鑑本文的思路,只是代碼上須要加些改動併發

兩種實現思路分佈式

比較時間戳ide

維護一個 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;

    }
}

 

 

隊列踢出this

 

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;
    }

}

 

比較兩種方法spa

  1. 第一種方法邏輯簡單粗暴, 只維護一個key-value 不須要使用鎖,非要說缺點的話沒有第二種方法靈活。
  2. 第二種方法我很喜歡,代碼很優雅靈活,可是邏輯相對麻煩一些,並且爲了保證線程安全地操做隊列,要使用分佈式鎖。目前咱們項目中使用的是第一種方法

本人免費整理了Java高級資料,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G,須要本身領取。
傳送門:https://mp.weixin.qq.com/s/osB-BOl6W-ZLTSttTkqMPQ線程

相關文章
相關標籤/搜索