• 【Redis】Redis 的共享 session 应用(短信登录)


    1. 基于 session 实现短信登录

    1.1 短信登录流程图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fWdekIX2-1660225714621)(C:/Users/bbbbbge/Pictures/接单/1660204636899.png)]

    1.2 实现发送短信验证码

    前端请求说明:

    说明
    请求方式POST
    请求路径/user/code
    请求参数phone(电话号码)
    返回值

    后端接口实现:

    @Slf4j
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
        @Override
        public Result sendCode(String phone, HttpSession session) {
            // 1. 校验手机号
            if(RegexUtils.isPhoneInvalid(phone)){
                // 2. 如果不符合,返回错误信息
                return Result.fail("手机号格式错误!");
            }
            // 3. 符合,生成验证码(设置生成6位)
            String code = RandomUtil.randomNumbers(6);
            // 4. 保存验证码到 session
            session.setAttribute("code", code);
            // 5. 发送验证码(这里并未实现,通过日志记录)
            log.debug("发送短信验证码成功,验证码:{}", code);
            // 返回 ok
            return Result.ok();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    1.3 实现短信验证码登录、注册

    前端请求说明:

    说明
    请求方式POST
    请求路径/user/login
    请求参数phone(电话号码);code(验证码)
    返回值

    后端接口实现:

    @Slf4j
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
        @Override
        public Result login(LoginFormDTO loginForm, HttpSession session) {
            // 1. 校验手机号
            String phone = loginForm.getPhone();
            if(RegexUtils.isPhoneInvalid(phone)){
                // 不一致,返回错误信息
                return Result.fail("手机号格式错误!");
            }
            // 2. 校验验证码
            String cacheCode = (String) session.getAttribute("code");
            String code = loginForm.getCode();
            if(cacheCode == null || !cacheCode.equals(cacheCode)){
                // 不一致,返回错误信息
                return Result.fail("验证码错误!");
            }
            // 4. 一致,根据手机号查询用户(这里使用的 mybatis-plus)
            User user = query().eq("phone", phone).one();
            // 5. 判断用户是否存在
            if(user == null){
                // 6. 不存在,创建新用户并保存
                user = createUserWithPhone(phone);
            }
            	// 7. 保存用户信息到 session 中(通过 BeanUtil.copyProperties 方法将 user 中的信息过滤到 UserDTO 上,即用来隐藏部分信息)
            session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
            return Result.ok();
        }
    
        private User createUserWithPhone(String phone) {
            // 1. 创建用户
            User user = new User();
            user.setPhone(phone);
            user.setNickName("user_" + RandomUtil.randomString(10));
            // 2. 保存用户(这里使用 mybatis-plus)
            save(user);
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    1.4 实现登录校验拦截器

    登录校验拦截器实现:

    public class LoginInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1. 获取 session
            HttpSession session = request.getSession();
            // 2. 获取 session 中的用户
            UserDTO user = (UserDTO) session.getAttribute("user");
            // 3. 判断用户是否存在
            if(user == null){
                // 4. 不存在,拦截,返回 401 未授权
                response.setStatus(401);
                return false;
            }
            // 5. 存在,保存用户信息到 ThreadLocal
            UserHolder.saveUser(user);
            // 6. 放行
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            // 移除用户,避免内存泄露
            UserHolder.removeUser();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    UserHolder 类的实现: 该类中定义了一个静态的 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    配置拦截器:

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                    .excludePathPatterns(
                            "/user/login",
                            "/user/code"
                    );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    前端请求说明:

    说明
    请求方式POST
    请求路径/user/me
    请求参数
    返回值

    后端接口实现:

    @Slf4j
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
        @Override
        public Result me() {
            UserDTO user = UserHolder.getUser();
            return Result.ok(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2. 集群的 session 共享问题

    session 共享问题:

    多台 tomcat 并不共享 session 存储空间,当请求切换到不同 tomcat 服务时会导致数据丢失的问题。

    session 的替代方案应该满足以下条件:

    • 数据共享(不同的 tomcat 都能够访问 Redis 中的数据)
    • 内存存储(Redis 通过内存存储)
    • key、value 结构(Redis 是 key-value 结构)

    3. 基于 Redis 实现共享 session 登录

    3.1 Redis 实现共享 session 登录流程图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vftmEWz7-1660225714622)(C:/Users/bbbbbge/Pictures/接单/1660220412262.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BVTivOmS-1660225714623)(C:/Users/bbbbbge/Pictures/接单/1660220567802.png)]

    3.2 实现发送短信验证码

    前端请求说明:

    说明
    请求方式POST
    请求路径/user/code
    请求参数phone(电话号码)
    返回值

    后端接口实现:

    @Slf4j
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public Result sendCode(String phone, HttpSession session) {
            // 1. 校验手机号
            if (RegexUtils.isPhoneInvalid(phone)) {
                // 2. 如果不符合,返回错误信息
                return Result.fail("手机号格式错误!");
            }
            // 3. 符合,生成验证码(设置生成6位)
            String code = RandomUtil.randomNumbers(6);
            // 4. 保存验证码到 Redis(以手机号为 key,设置有效期为 2min)
            stringRedisTemplate.opsForValue().set("login:code:" + phone, code, 2, TimeUnit.MINUTES);
            // 5. 发送验证码(这里并未实现,通过日志记录)
            log.debug("发送短信验证码成功,验证码:{}", code);
            // 返回 ok
            return Result.ok();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3.3 实现短信验证码登录、注册

    前端请求说明:

    说明
    请求方式POST
    请求路径/user/login
    请求参数phone(电话号码);code(验证码)
    返回值

    后端接口实现:

    @Slf4j
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
        @Override
        public Result login(LoginFormDTO loginForm, HttpSession session) {
            // 1. 校验手机号
            String phone = loginForm.getPhone();
            if(RegexUtils.isPhoneInvalid(phone)){
                // 不一致,返回错误信息
                return Result.fail("手机号格式错误!");
            }
            // 2. 校验验证码
            String cacheCode = (String) session.getAttribute("code");
            String code = loginForm.getCode();
            if(cacheCode == null || !cacheCode.equals(cacheCode)){
                // 不一致,返回错误信息
                return Result.fail("验证码错误!");
            }
            // 4. 一致,根据手机号查询用户(这里使用的 mybatis-plus)
            User user = query().eq("phone", phone).one();
            // 5. 判断用户是否存在
            if(user == null){
                // 6. 不存在,创建新用户并保存
                user = createUserWithPhone(phone);
            }
            	// 7. 保存用户信息到 session 中(通过 BeanUtil.copyProperties 方法将 user 中的信息过滤到 UserDTO 上,即用来隐藏部分信息)
            session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
            return Result.ok();
        }
    
        private User createUserWithPhone(String phone) {
            // 1. 创建用户
            User user = new User();
            user.setPhone(phone);
            user.setNickName("user_" + RandomUtil.randomString(10));
            // 2. 保存用户(这里使用 mybatis-plus)
            save(user);
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    3.4 实现登录校验拦截器

    这里将原有的一个拦截器分成两个拦截器,第一个拦截器对所有的请求进行拦截,每次拦截刷新 token 的有效期,并将能查询到的用户信息保存到 ThreadLocal 中。第二个拦截器则进行拦截功能,对需要登录的路径进行拦截。

    刷新 token 拦截器实现:

    public class RefreshTokenInterceptor implements HandlerInterceptor {
    
        private StringRedisTemplate stringRedisTemplate;
    
        public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1. 获取请求头中的 token
            String token = request.getHeader("authorization");
            if (StrUtil.isBlank(token)) {
                return true;
            }
            // 2. 基于 token 获取 redis 中的用户
            String tokenKey = "login:token:" + token;
            Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
            // 3. 判断用户是否存在
            if (userMap.isEmpty()) {
                return true;
            }
            // 5. 将查询到的 Hash 数据转为 UserDTO 对象
            UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
            // 6. 存在,保存用户信息到 ThreadLocal
            UserHolder.saveUser(user);
            // 7. 刷新 token 有效期 30 min
            stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES);
            // 8. 放行
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    登录校验拦截器实现:

    public class LoginInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1. 获取 session
            HttpSession session = request.getSession();
            // 2. 获取 session 中的用户
            UserDTO user = (UserDTO) session.getAttribute("user");
            // 3. 判断用户是否存在
            if(user == null){
                // 4. 不存在,拦截,返回 401 未授权
                response.setStatus(401);
                return false;
            }
            // 5. 存在,保存用户信息到 ThreadLocal
            UserHolder.saveUser(user);
            // 6. 放行
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            // 移除用户,避免内存泄露
            UserHolder.removeUser();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    UserHolder 类的实现: 该类中定义了一个静态的 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    配置拦截器:

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                    .addPathPatterns("/**").order(0);
            registry.addInterceptor(new LoginInterceptor())
                    .excludePathPatterns(
                            "/user/login",
                            "/user/code"
                    ).order(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    前端请求说明:

    说明
    请求方式POST
    请求路径/user/me
    请求参数
    返回值

    后端接口实现:

    @Slf4j
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
        @Override
        public Result me() {
            UserDTO user = UserHolder.getUser();
            return Result.ok(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    计算机毕业设计选题推荐-大学生班级管理系统-Python项目实战
    京东笔试-校招
    EPICS记录参考--模拟输入记录(ai)
    Redis的持久化机制和配置
    控制器-----controller
    leetcode152 乘积最大子数组
    pinia
    Pass-中间件管理
    Transmodeler中的路边停车,让车停进去的参数在哪儿设置?
    阅读openfoam框图
  • 原文地址:https://blog.csdn.net/weixin_51367845/article/details/126293976