实战优化登录系统:实现是否支持多设备、最大设备数等可配置化

使用场景:

有些用户可能需要在多台设备中登录同一个账户,同时希望设置可以登录的设备数。举个例子:公司的账户只允许五个员工登录系统。

实现方案:

利用redis的Zset有序集合,使用登录的当前时间戳作为分数,后续达到最大设备数之后,删除分数最小的,也就是登录时间最早的设备,实现强制退出。

登录时需要保存token

配置信息读取yaml文件

# 用户登陆配置
user-login:
  # token 到期时间 单位秒
  token-expire: 7200
  # 是否允许多设备登录              
  allowMultiDevice: true   
  # 最大允许设备数
  maxDevices: 5 
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "user-login")
public class UserLoginConfig {

    /**
     * token过期时间:单位秒
     */
    private Long tokenExpire;

    /**
     * 是否允许多设备登录
     */
    private Boolean allowMultiDevice;

    /**
     * 最大允许设备数
     */
    private Integer maxDevices;

}
     @Resource
    private RedissonClient redisson;

    @Resource
    private UserLoginConfig userLoginConfig;


 /**
     * 登录设备配置化:向 ZSet 中添加元素,只能添加设置的登录设备数量
     *
     * @param accessToken 元素的值
     * @param jwtToken 最新的登录jwtToken
     */
    public void addToZSetByLoginDevice(String accessToken, String accountCode, String jwtToken) {
        // 1.将AccountCode与accessToken关联起来(用来管理一个账户的多个accessToken)
        String key = RedisKeyConstants.USER_CURRENT_TOKEN + accountCode;
        // 获取 RScoredSortedSet 实例
        RScoredSortedSet zSet = redisson.getScoredSortedSet(key);

        // 支持多设备登录,并判断设备数量
        if (userLoginConfig.getAllowMultiDevice()) {
            // 检查 ZSet 中的元素数量
            int size = zSet.size();
            if (size >= userLoginConfig.getMaxDevices()) {
                // 计算需要删除的元素数量,+1因为后续要加入新的数据
                int elementsToRemove = size - userLoginConfig.getMaxDevices() + 1;
                // 循环删除分数最小的元素
                for (int i = 0; i < elementsToRemove; i++) {
                    // 获取分数最小的元素,也就是登录最早的token
                    String oldAccessToken = zSet.first();
                    if (oldAccessToken != null) {
                        // 删除分数最小的元素
                        zSet.remove(oldAccessToken);
                        // 删除旧Token关联的Redis键
                        this.del(oldAccessToken);
                        log.info("账户{},设备{},已强制退出", accountCode, oldAccessToken);
                    }
                }
            }
            // 刷新accessToken与JWT的映射(用于按Token验证),把之前的设备也刷新成最新的token
            if (size > 0) {
                for (String oldAccessToken : zSet) {
                    this.setex(oldAccessToken, userLoginConfig.getTokenExpire(), jwtToken);
                }
            }
        } else {
            // 如果不支持多设备登录,则直接清除
            Collection oldAccessTokenSet = zSet.readAll();
            if (oldAccessTokenSet != null && !oldAccessTokenSet.isEmpty()) {
                for (String oldAccessToken : oldAccessTokenSet) {
                    // 删除旧Token关联的Redis键
                    this.del(oldAccessToken);
                    log.info("账户{},设备{},已强制退出", accountCode, oldAccessToken);
                }
            }
            zSet.clear();
        }

        // 向 ZSet 中添加新的元素
        // 获取当前时间戳作为分数
        double score = new Date().getTime();
        // 每次添加元素时都会自动使用当前时间戳作为分数,从而保证 ZSet 中始终保留最大设备数中最新的元素
        zSet.add(score, accessToken);
        log.info("账户{},设备{},成功登录,目前登录设备数{}", accountCode, accessToken, zSet.size());

        // 2. 存储新Token到Redis,设置过期时间
        // 存储accountCode与JWT的映射(用于按用户标识查询)
        this.setex(RedisKeyConstants.ACCESS_TOKEN + accountCode, userLoginConfig.getTokenExpire(), jwtToken);
        // 存储最新的accessToken与JWT的映射(用于按Token验证)
        this.setex(accessToken, userLoginConfig.getTokenExpire(), jwtToken);
    }

退出时需要删除

  /**
     * 

退出系统,处理token

* * @param accountCode 账户code * @param authorization 请求头的访问token * @author wujiada * @since 2025/2/18 17:26 */ public void logoutRemoveToken(String accountCode, String authorization) { // 同一个账户有个多个accessToken String key = RedisKeyConstants.USER_CURRENT_TOKEN + accountCode; // 获取 RScoredSortedSet 实例 RScoredSortedSet zSet = redisson.getScoredSortedSet(key); // 检查 ZSet 中的元素数量 int size = zSet.size(); // 如果开启了多设备登录,需要判断是否还有其他设备,如果有,则不允许清除accessToken和jwtToken的关联 if (userLoginConfig.getAllowMultiDevice()) { if (size > 1) { // 访问token this.del(authorization); // token和设备的关联 zSet.remove(authorization); } else { // 当前只有一个设备,则直接清除accessToken和jwtToken的关联 this.del(RedisKeyConstants.ACCESS_TOKEN + accountCode); this.del(authorization); // token和设备的关联 zSet.clear(); } } else { // 如果不支持多设备,则直接清除accessToken和jwtToken的关联 this.del(RedisKeyConstants.ACCESS_TOKEN + accountCode); // 如果不支持多设备登录,则直接清除 Collection oldAccessTokenSet = zSet.readAll(); if (oldAccessTokenSet != null && !oldAccessTokenSet.isEmpty()) { for (String oldAccessToken : oldAccessTokenSet) { // 删除旧Token关联的Redis键 this.del(oldAccessToken); log.info("账户{},设备{},已强制退出", accountCode, oldAccessToken); } } // token和设备的关联 zSet.clear(); } log.info("账户{},设备{},成功退出,目前登录设备数{}", accountCode, authorization, zSet.size()); }

重置密码时需要删除token

    /**
     * 

更新密码,处理token

* * @param accountCode 账户code * @author wujiada * @since 2025/2/18 17:26 */ public void updatePasswordRemoveToken(String accountCode) { // 更新密码需要,需要删除全部的token数据 // 同一个账户有个多个accessToken String key = RedisKeyConstants.USER_CURRENT_TOKEN + accountCode; // 获取 RScoredSortedSet 实例 RScoredSortedSet zSet = redisson.getScoredSortedSet(key); this.del(RedisKeyConstants.ACCESS_TOKEN + accountCode); Collection oldAccessTokenSet = zSet.readAll(); if (oldAccessTokenSet != null && !oldAccessTokenSet.isEmpty()) { for (String oldAccessToken : oldAccessTokenSet) { // 删除该账户下所有登录设备的访问token this.del(oldAccessToken); log.info("账户{},设备{},已强制退出", accountCode, oldAccessToken); } } // token和设备的关联 zSet.clear(); }

刷新token过期时间时需要刷新当前设备的分数

    /**
     * 

token续期

* @author wujiada * @since 2025/1/21 11:51 */ public void renewToken(String accessToken, String claims) { RBucket rBucket = this.redisson.getBucket(accessToken); // token续期 // 判断过期时间是否少于60分钟,如果小于,则续期 long remainTime = rBucket.remainTimeToLive(); // 60 分钟的毫秒数 long thirtyMinutesInMillis = TimeUnit.MINUTES.toMillis(60); if (remainTime < thirtyMinutesInMillis) { String accountCode = Base64Util.decode(claims); // 续期成功之后,同一个账户关联多个accessToken,需要将续期成功的作为新增的加入到Zset集合中,避免达到最大设备数时将其删除 RScoredSortedSet zSet = redisson.getScoredSortedSet(RedisKeyConstants.USER_CURRENT_TOKEN + accountCode); RBucket rBucket1 = this.redisson.getBucket(RedisKeyConstants.ACCESS_TOKEN + accountCode); // 设置新的过期时间 Long tokenExpire = userLoginConfig.getTokenExpire(); rBucket.expire(tokenExpire, TimeUnit.SECONDS); rBucket1.expire(tokenExpire, TimeUnit.SECONDS); // 获取当前时间戳作为分数,时间越新,时间戳越大。 double score = new Date().getTime(); zSet.add(score, accessToken); } }

总结:

通过以上的方法可以实现同一个账户多设备登录、设备数量进行可配置化操作。

你可能感兴趣的:(#,实战优化,java)