• 黑马点评回顾 redis实现共享session


    传统session缺点

    传统单体项目一般是把session存入tomcat,但是每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题。为了解决这个问题,我们用redis作为中间件,把用户的信息存入redis中,这样不同tomcat在检验用户是否登陆时,都向redis请求数据,这样就可以在不同的tomcat进行session共享。

    整体访问流程

    用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。
    在这里插入图片描述

    代码实现

    生成验证码

    生成验证码,并把生成的验证码存入redis的代码。

        public Result sendCode(String phone) {
            // 1.校验手机号
            if (RegexUtils.isPhoneInvalid(phone)) {
                // 2.如果不符合,返回错误信息
                return Result.fail("手机号格式错误!");
            }
            // 3.符合,生成验证码
            String code = RandomUtil.randomNumbers(6);
    
            // 4.保存验证码到 session
            stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, 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

    登录

    登陆的功能是前端传来,用户的手机号和验证码,如果手机号和验证码都正确,就判断用户是否存在,存在就登陆,不存在就创建用户,然后登陆,如果手机号或者验证码其中一个不正确,就返回错误。

     public Result login(LoginFormDTO loginForm) {
            // 1.校验手机号
            String phone = loginForm.getPhone();
            if (RegexUtils.isPhoneInvalid(phone)) {
                // 2.如果不符合,返回错误信息
                return Result.fail("手机号格式错误!");
            }
            // 3.从redis获取验证码并校验
            String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
            String code = loginForm.getCode();
            if (cacheCode == null || !cacheCode.equals(code)) {
                // 不一致,报错
                return Result.fail("验证码错误");
            }
    
            // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
            User user = query().eq("phone", phone).one();
    
            // 5.判断用户是否存在
            if (user == null) {
                // 6.不存在,创建新用户并保存
                user = createUserWithPhone(phone);
            }
    
            // 7.保存用户信息到 redis中
            // 7.1.随机生成token,作为登录令牌
            String token = UUID.randomUUID().toString(true);
            // 7.2.将User对象转为HashMap存储
            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()));
            // 7.3.存储
            String tokenKey = LOGIN_USER_KEY + token;
            stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
            // 7.4.设置token有效期
            stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    
            // 8.返回token
            return Result.ok(token);
        }
    
    • 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
    • 42

    问题

    上面的代码仍然存在问题,就是session刷新问题,原本利用HttpSession,每次访问就刷新,session维持时间就会刷新,然而我们上面的代码是利用redis作为中间件共享session不能自动刷新,为了解决这个问题,我们需要自己设置拦截器进行手动刷新。

    具体思路

    首先我们要配置一个拦截器,它把他的优先设为最高,他拦截一切路径,然后它的方法内每次拦截的时候,都会检查redis中是否有session,如果有就刷新session,这样就可以维持session维持的时间。
    设置拦截器

    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 key  = LOGIN_USER_KEY + token;
            Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
            // 3.判断用户是否存在
            if (userMap.isEmpty()) {
                return true;
            }
            // 5.将查询到的hash数据转为UserDTO
            UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
            // 6.存在,保存用户信息到 ThreadLocal
            UserHolder.saveUser(userDTO);
            // 7.刷新token有效期
            stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
            // 8.放行
            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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    添加拦截器

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 登录拦截器
            registry.addInterceptor(new LoginInterceptor())
                    .excludePathPatterns(
                            "/shop/**",
                            "/voucher/**",
                            "/shop-type/**",
                            "/upload/**",
                            "/blog/hot",
                            "/user/code",
                            "/user/login"
    //                        值越小优先级越高。
                    ).order(1);
            // token刷新的拦截器
            registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
  • 相关阅读:
    Linux环境基础开发工具使用
    在linux系统中安装nginx
    Python简单实现人脸识别检测, 对照片进行评分
    C++从零实现神经网络
    【Java并发】聊聊ReentrantReadWriteLock锁降级和StampedLock邮戳锁
    【【萌新的SOC学习之小水文系列】】
    【快应用】webview接口打开网页,网页会被自动放大,如何适配手机大小
    【C++】命名空间、缺省参数和函数重载
    指望识别技术
    强制删除k8s中的命名空间为 Terminating 的ns
  • 原文地址:https://blog.csdn.net/studycodeday/article/details/134402950