<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot 并發(fā)登錄人數(shù)控制

          共 4987字,需瀏覽 10分鐘

           ·

          2021-04-24 20:56

          轉(zhuǎn)自:簡書,作者:殷天文

          www.jianshu.com/p/b6f5ec98d790

          通常系統(tǒng)都會限制同一個賬號的登錄人數(shù),多人登錄要么限制后者登錄,要么踢出前者,Spring Security 提供了這樣的功能,本文講解一下在沒有使用Security的時候如何手動實(shí)現(xiàn)這個功能。


          demo 技術(shù)選型


          • SpringBoot

          • JWT

          • Filter

          • Redis + Redisson


          JWT(token)存儲在Redis中,類似 JSessionId-Session的關(guān)系,用戶登錄后每次請求在Header中攜帶jwt


          如果你是使用session的話,也完全可以借鑒本文的思路,只是代碼上需要加些改動


          兩種實(shí)現(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;

          }
          }


          隊(duì)列踢出



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

          // 如果隊(duì)列里沒有此token,且用戶沒有被踢出;放入隊(duì)列
          if (!deque.contains(token) && currentSession.isKickout() == false) {
          deque.push(token);
          }

          // 如果隊(duì)列里的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;
          }

          }


          比較兩種方法


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

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


          演示


          下載地址:

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


          1. 運(yùn)行項(xiàng)目,訪問localhost:8887 demo中沒有存儲用戶信息,隨意輸入用戶名密碼,用戶名相同則被踢出

          2. 訪問 localhost:8887/index.html 彈出用戶信息, 代表當(dāng)前用戶有效

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

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


          本文借鑒了https://jinnianshilongnian.iteye.com/blog/2039760, 如果你是使用 Shiro + Session 的模式,推薦閱讀此文



          --  end  --


          喜歡就三連



          關(guān)注 Stephen,一起學(xué)習(xí),一起成長。

          瀏覽 15
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  夜夜撸日日操 | 国产精品 亚洲无码 | 狠狠狠狠狠插狠狠狠插狠狠狠插 | 人人看人人干 | www黄色一级 |