1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        兩種方法,如何實現(xiàn) SpringBoot 并發(fā)登錄人數(shù)控制,然后再對比下!...

        共 4789字,需瀏覽 10分鐘

         ·

        2020-07-11 12:30

        來源:jianshu.com/p/b6f5ec98d790

        • demo 技術(shù)選型
        • 兩種實現(xiàn)思路
          • 比較時間戳
          • 隊列踢出
        • 比較兩種方法
        • 演示

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

        本文借鑒了

        https://jinnianshilongnian.iteye.com/blog/2039760

        如果你是使用 Shiro + Session 的模式,可以閱讀此文

        demo 技術(shù)選型
        • SpringBoot
        • JWT
        • Filter
        • Redis + Redisson

        JWT(token)存儲在Redis中,類似 JSessionId-Session的關(guān)系,用戶登錄后每次請求在Header中攜帶jwt 如果你是使用session的話,也完全可以借鑒本文的思路,只是代碼上需要加些改動

        兩種實現(xiàn)思路

        比較時間戳

        維護一個 username: jwtToken 這樣的一個 key-value 在Reids中, Filter邏輯如下

        ecec515435131cd7eac7f0f64b7a86a1.webp圖片不清可點開放大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?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,?"您的賬號已在其他設(shè)備登錄");
        ????????????????return?false;

        ????????????}

        ????????}

        ????????return?true;

        ????}
        }

        隊列踢出

        c4adba3a7576b110be195d064fe40a3f.webpimgpublic?class?QueueKickOutFilter?extends?KickOutFilter?{
        ????/**
        ?????*?踢出之前登錄的/之后登錄的用戶?默認踢出之前登錄的用戶
        ?????*/

        ????private?boolean?kickoutAfter?=?false;
        ????/**
        ?????*?同一個帳號最大會話數(shù)?默認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?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?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;
        ????}

        }
        比較兩種方法

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

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

        演示

        下載地址:

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

        • 運行項目,訪問localhost:8887 demo中沒有存儲用戶信息,隨意輸入用戶名密碼,用戶名相同則被踢出
        • 訪問 localhost:8887/index.html 彈出用戶信息, 代表當前用戶有效
        • 另一個瀏覽器登錄相同用戶名,回到第一個瀏覽器刷新頁面,提示被踢出
        • application.properties中選擇開啟哪種過濾器模式,默認是比較時間戳踢出,開啟隊列踢出 queue-filter.enabled=true
        瀏覽 34
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            男女插下面视频 | 豆花视频在线视频 | 日韩色图在线观看 | 亚洲欧美日韩中文在线 | 我要操你的逼 | 成人高清无码视频在线观看 | 成人免费黄色大片 | 众多明星短篇乱淫小说 | 久久久久久麻豆 | 奇米影视无码不卡在线视频 |