关于java:SpringBoot-并发登录人数控制

11次阅读

共计 3401 个字符,预计需要花费 9 分钟才能阅读完成。

公众号:MarkerHub,网站:https://markerhub.com

小 Hub 领读:

作者聊了两种剔除形式:1、比拟 jwt 创立工夫戳,2、应用队列保障 token 惟一,但须要用锁保障线程平安。你们个别用哪种?


作者:殷地理

https://www.jianshu.com/p/b6f…

通常零碎都会限度同一个账号的登录人数,多人登录要么限度后者登录,要么踢出前者,Spring Security 提供了这样的性能,本文解说一下在没有应用 Security 的时候如何手动实现这个性能

本文借鉴了 https://jinnianshilongnian.iteye.com/blog/2039760, 如果你是应用 Shiro + Session 的模式,举荐浏览此文

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

            
            if (tokenUnixTime.compareTo(redisTokenUnixTime) > 0) {bucket.set(token);

            } else {userService.logout(token);
                sendJsonResponse(response, 4001, "您的账号已在其余设施登录");
                return false;

            }

        }

        return true;

    }
}

队列踢出

public class QueueKickOutFilter extends KickOutFilter {
    
    private boolean kickoutAfter = false;
    
    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);

            
            if (!deque.contains(token) && currentSession.isKickout() == false) {deque.push(token);
            }

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

}

比拟两种办法

  1. 第一种办法逻辑简略粗犷, 只保护一个 key-value 不须要应用锁,非要说毛病的话没有第二种办法灵便。
  2. 第二种办法我很喜爱,代码很优雅灵便,然而逻辑绝对麻烦一些,而且为了保障线程平安地操作队列,要应用分布式锁。目前咱们我的项目中应用的是第一种办法

演示

下载地址: https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/login-control

  1. 运行我的项目,拜访 localhost:8887 demo 中没有存储用户信息,随便输出用户名明码,用户名雷同则被踢出
  2. 拜访 localhost:8887/index.html 弹出用户信息, 代表以后用户无效
  3. 另一个浏览器登录雷同用户名,回到第一个浏览器刷新页面,提醒被踢出
  4. application.properties 中抉择开启哪种过滤器模式,默认是比拟工夫戳踢出,开启队列踢出 queue-filter.enabled=true

(完)

举荐浏览

太赞了,这个 Java 网站,什么我的项目都有!https://markerhub.com

这个 B 站的 UP 主,讲的 java 真不错!

太赞了!最新版 Java 编程思维能够在线看了!

正文完
 0