Redis - 数据共享 Session

项目流程

  1. 客户端 向 NGINX 请求静态页面
  2. NGINX 向 tomcat 服务端请求数据
  3. 数据库集群将数据返回给 tomcat
  4. comcat 将数据返回给 NGINX
  5. NGINX 将数据返回给客户端的页面

在这里插入图片描述


相关工具类

DTO

  1. 功能:DataTransferObject,隐藏用户的敏感信息,将含有用户敏感信息的User对象转化成没有敏感信息的UserDto对象
  2. 实现:BeanUtils.copyProperties( user, UserDTP.class)
    1. 注:此方法不要求源对象和目标类之间有继承或接口实现关系,只要属性名相同且类型兼容即可

HttpSession

  1. 功能:在服务器端管理用户会话,用于在多个 HTTP 请求之间保存用户状态信息

  2. 常用方法

    方法功能
    1. setAttribute(”attributeName”, attributeValue);设置(添加)属性
    2. getAttribute(”attributeName”, attributeValue);获取属性
    3. removeAttribute(“attributeName”);移除属性
    4. String getId()获取会话 ID
    5. setMaxInactiveInterval(30 * 60)设置最大不活动间隔
    6. invalidate()使会话无效

UserHolder

  1. 功能:通过 ThreadLocal 维持一个用户线程
  • 实现:通过 java.lang 的 ThreadLocal 实现

    public class UserHolder {
        private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
    
        public static void saveUser(UserDTO user){
            tl.set(user);
        }
    
        public static UserDTO getUser(){
            return tl.get();
        }
    
        public static void removeUser(){
            tl.remove();
        }
    }
    

登录功能 (MySQL)

  1. 功能:接收客户端请求,验证用户手机号和验证码,返回登录结果

在这里插入图片描述

发送短信验证码

  1. UserController
    1. 创建函数:sendCode(@RequestParam(”phone”) String phone, HttpSession session)
    2. 调用 Service 的服务:userService.sendCode(phone, session);
    3. 返回 Service 的结果
  2. UserService
    1. 创建方法:Result getCode(String phone, HttpSession session)
  3. UserServiceImpl
    1. 实现方法:Result getCode(String phone, HttpSession session)
    2. 校验手机号格式 (错误则提前退出,返回 Result.fail(”格式错误!”)
    3. 生成验证码
    4. 验证码保存至 Session
    5. 发送验证码
    6. 返回 Result.ok()

短信验证码登录 & 注册

  1. UserController
    1. 创建函数: login(@RequestBody LoginFormDTO loginForm, HttpSession session)
    2. 调用 Service 的服务:userService.login( loginForm, session);
    3. 返回 Service 的结果
  2. UserService
    1. 创建函数 Result login( String code, HttpSession session )
  3. UserServiceImpl
    1. 实现函数 Result login( LoginFormDTO loginForm, HttpSession session)
    2. 校验 logForm 中的手机号格式 (不符合则提前退出,返回 Result.fail(”手机号格式错误!”)
    3. 校验 session 中的验证码是否和用户填写的 code 相符合 (不符合则提前退出,返回 Result.fail(”验证码错误!”)
    4. 查询手机用户:User user = query().eq( “phone”, phone).one(); ( 注:query() 来自 mybatisPlus)
    5. 不存在则创建用户 user = createUserWithPhone(phone); ( 注:createUserWithPhone() 需要自己实现)
    6. 保存用户信息到 Session 中
    7. 返回 Result.ok()

校验登录状态

  1. LoginInterceptor
    1. 实现 HandlerInterceptor 接口类
    2. 实现 preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) 方法
      1. 获取 Session:HttpSession session = request.getSession();
      2. 校验 Session 中的用户 Object user = session.getAttribute(”user”);
      3. 用户不存在则给 Response 写入 401 状态码 ( response.setStatus(401) ),并返回 false
      4. 用户存在则将其添加到 ThreadHolder 中,其中 ThreadHolder 用 UserHolder 管理
    3. 实现 afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 方法
      1. 释放用户连接:UserHolder.removeUser()
  2. MvcConfig
    1. 放置在 config 包目录下
    2. 注解为 @Configuration
    3. 实现 WebMvcConfigurer 接口
    4. 重写 addInterceptors( InterceptorRegistry registry) 方法
    5. 添加 Interceptor:registry.addInterceptor( new LoginInterceptor() ).excludePathPatterns( “/**”, …)

登录数据共享(Redis)

集群 Session 共享

  1. 问题引入:多台 Tomcat 服务器不共享 session 存储空间,请求切换到不同 Tomcat 服务器会导致数据丢失
  2. 目标:通过共享 Redis 数据,解决 Session 在不同 Tomcat 服务器之间 Session 信息无法共享的问题
  3. 途径:使用 Redis 共享数据 (之前是在 Tomcat 服务器中存储 Session 信息,现在存储在 Redis 中)

重点

  1. 数据结构
    1. String:每个对象的键都会单独存储,多占用一点空间,如果要修改单个键的值则需要全部重新输入
    2. HashMap:每个对象只会存储数据本身,方便修改单个键的值
  2. key 结构:”prefix” + “randomString”
    1. 唯一性:随机生成 UUID 作为 Redis 查询的 Key
    2. 安全性:手机号唯一但是存在隐私问题,故不采用
  3. 存储粒度:传输不含隐私信息的 FormDTO(Data Transform Object)

校验登录状态 ( Session → Redis )

  1. 修改 login( LoginFormDTO loginForm, HttpSession, session)

  2. 🆕 保存用户登录信息

    1. 生成 token 作为登录令牌

      String token = UUID.randomUUID().toString(true);
      
    2. 保护隐私信息 & 转换存储格式:转换 User 类为 HashMap 存储,方便存入 Redis

      stringRedisTemplate 无法处理非 String 属性,需要自定义 CopyOptions 将 HashMap 中的 value 转为 String

      UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
      Map<String, Object> userMap = BeanUtil.beanToMap( userDTO, new HashMap<>(),
              CopyOptions.create().
      	        setIgnoreNullValue(true).
      	        setFieldValueEditor((fieldName, fieldValue) → fieldValue.toString()));
      
    3. 存储 key-value 到 Redis 中

      String tokenKey = LOGIN_USER_KEY + token;
      stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
      
    4. 设置 token 有效期 :有效期内验证登录状态可以查询到 token,实现逻辑登录校验

      stringRedisTemplate.expire( tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
      
  3. 返回 token 给客户端 (不需要 prefix ),后续通过查询 Redis 中是否含有 token 来判断登录状态

    return Result.ok(token);
    

问:为什么 tokenKey 要加上一个 LOGIN_USER_KEY 前缀?
答:因为 Redis 是公共存储空间,所有数据都加上前缀防止误用
问:为什么要在 util 包下创建 RredisConstants 类?
答:为了代码更加优雅,防止过多的硬编码

流程图

在这里插入图片描述


刷新登录状态

  1. 问题引入:用户只访问不需要拦截的路径,那么拦截器不会生效,令牌刷新的动作不会执行
  2. 目标:用户所有操作都会刷新 Session 有效期
  3. 途径:添加全局拦截器,用户操作任何操作都刷新 Session 有效期

RefreshTokenInterceptor

  1. 创建类:创建 RefreshTokenInterceptor 类,实现 HandlerInterceptor 接口

  2. 依赖注入:注入 StringRedisTemplate 依赖 (用钩子函数创建)

    private StringRedisTemplate stringRedisTemplate;
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
    	this.stringRedisTemplate = stringRedisTemplate;
    }
    
  3. 获取登录信息:基于 token 获取 Redis 中的用户

    String token = request.getHeader("authorization");
    if( StrUtil.isBlank(token) )  return true;           // 如果不存在token则不需要刷新,直接放行
    
  4. 判断用户是否存在 (不存在则提前退出,返回 true)

    String tokenKey = LOGIN_USER_KEY + token;
    Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
    if( userMap.isEmpty())  return true;
    
  5. 映射查询到的 hash 数据为 UserDTO

    UserDTO userDTO = BeanUtil.fillBeanWithMap( userMap, new UserDTO(), false);
    
  6. 保存用户信息到 ThreadLocal ( UserHolder )

    UserHolder.saveUser(userDTO);
    
  7. 刷新 token 有效期

    stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
    
  8. 返回 true;

LoginInterceptor

  1. 修改 LoginInterceptor 类,实现 HandlerInterceptor 接口

  2. 修改 preHndle 方法

  3. 判断 UserHolder 中是否有 User,没有则拦截,否则放行

    @Override
    public boolean preHandle(HtttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
    	if( UserHolder.getUser() == null){
    		response.setStatus(401);
    		return false;
    	}
    	return true;
    }
    

配置拦截器

  1. 修改 MvcConfig 类

  2. 注入 StringRedisTemplate 对象

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
  3. 修改 addInterceptors 方法

  4. 添加 RefreshTokenInterceptor 拦截器到 registry 中

    registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
    						.addPathPatterns("/**").order(0);
    
  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Shiro-Redis 是一个用于在 Shiro 中实现 Session 共享的插件,它使用 Redis 作为数据存储和缓存,以实现分布式环境下的 Session 共享。 要实现 Shiro-RedisSession 共享,你需要进行以下步骤: 1. 引入 Shiro-Redis 插件依赖:在项目的 Maven 或 Gradle 配置文件中添加 Shiro-Redis 依赖。 2. 配置 Redis 连接信息:在项目的配置文件中配置 Redis 的连接信息,包括主机名、端口号、密码等。 3. 配置 RedisSessionDAO:在 Shiro 的配置文件中配置 RedisSessionDAO,指定使用 Redis 作为 Session 存储和缓存的实现。可以设置过期时间、前缀等参数。 4. 配置 Session Manager:在 Shiro 的配置文件中配置 Session Manager,指定使用自定义的 RedisSessionManager 作为 Session 的管理器。同时,需要将之前配置的 RedisSessionDAO 设置给 RedisSessionManager。 5. 配置 SecurityManager:在 Shiro 的配置文件中配置 SecurityManager,指定使用自定义的 RedisSessionManager 作为 Session 管理器。同时,需要将之前配置的 RedisSessionDAO 设置给 RedisSessionManager。 6. 配置 Filter Chain:在 Shiro 的配置文件中配置 Filter Chain,将自定义的 RedisSessionManager 添加到 Filter Chain 中,以便对请求进行 Session 管理。 通过以上步骤配置完成后,Shiro 将会使用 Redis 进行 Session 的存储和缓存,从而实现 Session 的共享。在分布式环境中,不同应用节点之间可以通过 Redis 共享 Session 数据,从而实现用户的登录状态和会话信息的共享。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值