关于用redis实现单设备登录,单点登录,断连超时的问题

最近接到一个需求,需要实现一个账号只能在一台设备登录,并且兼容SSO单点登录以及断连超时的。

设计方案:

        登录的时候会产生两个token,一个长期token,一个短期token,并且把它记录到redis里面。短期token设置过期时间为三分钟。每次访问接口时都会在请求头上携带短期token。同时写了一个过滤器,在每次进入接口前会验证token的合法性。这样就解决了单设备和单点登录。

        详细解释一下,就是用户在每次登录时都会生成一个不同的token,并且在redis记录的时候是以用户ID作为key,token作为value存储,如果在第二个客户端登录时生成的新token会覆盖旧token,这样之前的客户端发起请求时就会无效。

        

        断连超时:第一个方案:登录的时候都会获取客户端的IP地址,用WebSocket进行IP直连。                   

                          第二个方案由客户端发起http请求(发送心跳包),并且请求的时候携带短期token,这里基于安全性考虑,每次在进行心跳请求的时候会验证该token的合法性,如果合法的话,进行刷新生成一个新的token,并且返回给客户端,客户端保存新的token,每次请求携带新的token。商议以后用的是该方案。

详细说明一下实现过程:

        首先在登录的时候先在redis上面存储数据,并且设置好过期时间,因为每个数据过期时间不一样,所以我每一条数据都是单独存放。验证登录的代码就不贴了,直接贴存redis代码:

stringRedisTemplate.opsForValue().set(redisUeKey + ":accessToken:", accessTokenDO.getAccessToken(), 60 * 24, TimeUnit.SECONDS);
            stringRedisTemplate.opsForValue().set(redisUeKey + ":refreshToken:", accessTokenDO.getRefreshToken(), 60 * 60 * 24 * 30, TimeUnit.SECONDS);

redis存放好以后。如果用户断线的话,又有两个方案供选择,

        第一个方案:写一个定时任务,每隔相应的时间就去redis里面读取哪些键值对还存在,我认为不可取。

        第二个方案,也是我用的方案:写一个redis的过期键监听器,去监控哪些键过期。

在使用第二个方案之前首先得在redis的配置文件里面,把这个功能开启。notify-keyspace-events 对应的值改为 Ex。

监听器:在redis的配置类里面创建监听。

@Bean
    public RedisMessageListenerContainer redisMessageListenerContainer() {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
        return redisMessageListenerContainer;
    }

创建好监听以后,写监听的逻辑:

@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    @Value("${spring.rabbitmq.host}")
    private String host;

    @Value("${spring.rabbitmq.username}")
    private String username;

    @Value("${spring.rabbitmq.password}")
    private String password;

    @Value("${spring.rabbitmq.virtual-host}")
    private String virtualHost;

    @Resource
    private StringRedisTemplate stringRedisTemplate;



    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    /***
     * @Author Feng 
     * @Date 2024/6/18 17:19
     * @Description 监听redis过期键
     * @Param [message, pattern]
     * @Return void
     * @Since version 1.0
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        //获得失效的key
        String expiredKey = new String(message.getBody());
        String[] parts = expiredKey.split(":");
        if (!"accessToken".equals(parts[2])){
            return;
        }
        String extractedString = parts[1]; 
        log.warn("用户: " + extractedString + "断连");
        String queueName = stringRedisTemplate.opsForValue().get("UE:" + extractedString + ":queueName:");
        if (queueName == null){
            log.info("断连用户没有创建queue");
            return;
        }
        stringRedisTemplate.delete("UE:" + extractedString + ":queueName:");
        try {
            Connection connection = createConnection(host, username, password, virtualHost);
            Channel channel = connection.createChannel();
            channel.queueDelete(queueName);
        } catch (IOException | TimeoutException e) {
            throw new RuntimeException(e);
        }
        log.info("断连用户queue删除成功: " + queueName);
        int i = userService.updateUserOffline(extractedString);
        if (i == 1){
            log.warn("用户: " + extractedString + "断连状态修改成功");
        }else {
            log.error("用户: " + extractedString + "断连状态修改失败");
        }
    }

}

   因为我这里夹杂了rabbitMQ需要实时删除,参考的话可以不用管。

这里就已经监听成功。并且如果用户断线成功修改用户的状态。

然后是单设备登录以及单点登录:

        首先先写一个过滤器:每次请求的时候都去请求头里面获取该token验证token的合法性。

package com.example.linyun_project.filter;

import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @description:
 * @author: Feng
 * @time: 2024/6/13 14:45
 */
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //校验用户登录状态
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //取出token
        String token = request.getHeader("accessToken");
        //取出userId
        String userId = request.getHeader("userId");

        if ("".equals(userId)){
            PrintWriter printWriter = response.getWriter();
            printWriter.write("未登录");
            printWriter.flush();
            printWriter.close();
            // 退出方法,不放行
            return;
        }

        if ("".equals(token)){
            PrintWriter printWriter = response.getWriter();
            printWriter.write("请重新登录");
            printWriter.flush();
            printWriter.close();
            return;
        }

        //检查携带的token是否和redis的token一致,如果一致的话就放行
        String accessToken = stringRedisTemplate.opsForValue().get("UE:" + userId + ":accessToken:");
        if (accessToken == null){
            PrintWriter printWriter = response.getWriter();
            printWriter.write("断连超时");
            printWriter.flush();
            printWriter.close();
            return;
        }

        if (!accessToken.equals(token)){
            PrintWriter printWriter = response.getWriter();
            printWriter.write("该账号已在其他地方登录");
            printWriter.flush();
            printWriter.close();
            return;
        }

        filterChain.doFilter(servletRequest, servletResponse);

    }
}

这样三个问题得到全部解决。单独说一下,因为登录和这些操作不是在一台服务器上面,所以我用的是匹配全部路径,如果要选择性修改路径的话,可以自行根据需求进行修改。

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值