Redis实战——短信登录(二)

Redis代替session

  • redis中设计key
    • 在使用session时,每个用户都会有自己的session,这样虽然验证码的键都是"code",但是相互不影响,从而确保每个用户获取到的验证码只能够自己使用,当使用redis时,redis的key是共享的,不分用户,就要求在redis中存储验证码时,不能直接将验证码的键设置为"code",这样无法保证其唯一性。
  • redis中设计value
    • 到底该使用redis中什么数据类型存储数据,主要需要看数据样式和使用方式,一般会考虑使用String、Hash,String存储时,会多占一点内存空间,则相对来说Hash存储时,会少占用一点内存空间。
      • String结构:以Json字符串来存储,比较直观
      • Hash结构:,每个对象中每个字段独立存储,可以针对单个字段做CRUD

redis实现登录

发送验证码
  • 添加redis

    pom 复制代码
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
  • 设置redis的连接信息

    yaml 复制代码
    spring:
      redis:
        host: 192.168.175.128
        port: 6379
        password: liang
        lettuce:
          pool:
            max-active: 10
            max-idle: 10
            min-idle: 1
            time-between-eviction-runs: 10s
  • 增加相关常量

    java 复制代码
    /**
     * 保存验证码的redis中的key
     */
    public static final String LOGIN_CODE_KEY = "login:code:";
    /**
     * 验证码的过期时间
     */
    public static final Long LOGIN_CODE_TTL = 2L;
  • 修改Service层

    java 复制代码
       @Autowired
        StringRedisTemplate stringRedisTemplate;
    
        @Override
        public boolean sendCode(String phone, HttpSession session) {
            //获取手机号,验证手机号是否合规
            boolean mobile = PhoneUtil.isMobile(phone);
            //不合规,则提示
            if (!mobile){
                return false;
            }
            //生成验证码
            String code = RandomUtil.randomNumbers(6);
            //保存验证码到redis,并设置过期时间
            stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
            //发送验证码,这里就通过打印验证码模拟了下发送验证码
            System.out.println("验证码:" + code);
            return true;
        }
  • 修改Controller层

    java 复制代码
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        String uuid = userService.sendCode(phone, session);
        return uuid.equals("") ? Result.fail("手机号码不合规"): Result.ok(uuid);
    }
验证码登录、注册
  • 增加相关常量

    java 复制代码
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;
  • 修改Service层

    java 复制代码
    @Override
    public String login(LoginFormDTO loginForm, HttpSession session) {
        //获取手机号
        String phone = loginForm.getPhone();
        //验证手机号是否合理
        boolean mobile = PhoneUtil.isMobile(phone);
        //如果不合理 提示
        if (!mobile){
            //提示用户手机号不合理
            return "";
        }
        //手机号合理 进行验证码验证
        String code = loginForm.getCode();
        //从redis中获取验证码
        String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        //如果验证码输入的是错误的  提示
        if (!code.equals(redisCode)){
            return "";
        }
        //如果验证码也正确 那么通过手机号进行查询
        User user = this.getOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
        // 数据库中没查询到用户信息
        if (ObjectUtil.isNull(user)){
            user = new User();
            user.setPhone(phone);
            user.setNickName("user_"+ RandomUtil.randomString(10));
            this.save(user);
        }
        // 将用户信息保存到Redis中,注意避免保存用户敏感信息
        UserDTO userDTO = BeanUtil.toBean(user, UserDTO.class);
        // 设置UUID保存用户信息
        String uuid = IdUtil.fastSimpleUUID();
        // 将user对象转化为Map,同时将Map中的值存储为String类型的
        Map<String, Object> userDTOMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().ignoreNullValue()
                        .setFieldValueEditor((key, value) -> value.toString()));
        stringRedisTemplate.opsForHash().putAll( LOGIN_USER_KEY + uuid, userDTOMap);
        //设置过期时间
        stringRedisTemplate.expire(LOGIN_USER_KEY + uuid, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 通过UUID生成简单的token
        String token = uuid + userDTO.getId();
        return token;
    }    
    java 复制代码
    String login(LoginFormDTO loginForm, HttpSession session);
  • 修改Controller层

    java 复制代码
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        String token = userService.login(loginForm, session);
        return StrUtil.isNotBlank(token) ? Result.ok(token) : Result.fail("手机号或验证码错误");
    }
校验登录状态
  • 修改LoginInterceptor拦截器

    java 复制代码
    private StringRedisTemplate stringRedisTemplate;
    
    /**
     * 构造函数
     * @param stringRedisTemplate
     */
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    
    /**
     * preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从请求头中获取token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            return false;
        }
    
        String uuid = token.substring(0,token.lastIndexOf("-"));
        System.out.println(uuid);
        //从redis中获取值
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + uuid);
        if (ObjectUtil.isNull(entries)){
            return false;
        }
        //将map转化为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), true);
        //将用户信息保存到 ThreadLocal
        UserHolder.saveUser(userDTO);
        return true;
    }
    java 复制代码
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Resource
        StringRedisTemplate stringRedisTemplate;
    
        /**
         * 添加拦截器
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //添加拦截器
            registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                    //放行资源
                    .excludePathPatterns(
                            "/shop/**",
                            "/voucher/**",
                            "/shop-type/**",
                            "/upload/**",
                            "/blog/hot",
                            "/user/code",
                            "/user/login"
                    )
                    // 设置拦截器优先级
                    .order(1);
        }
    }
登录状态的刷新问题
  • 因为设置了redis中存储的用户的有效期,所以在用户访问界面的时,需要更新token令牌的存活时间,例如修改LoginInterceptor拦截器,在此拦截器中刷新过期时间

    java 复制代码
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从请求头中获取token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            return false;
        }
    
        String uuid = token.substring(0,token.lastIndexOf("-"));
        System.out.println(uuid);
        //从redis中获取值
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + uuid);
        if (ObjectUtil.isNull(entries)){
            return false;
        }
        //将map转化为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), true);
        //将用户信息保存到 ThreadLocal
        UserHolder.saveUser(userDTO);
        //刷新token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + uuid, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }
  • 但是需要注意的是,自定义的登录拦截器只是针对需要登录访问的请求进行了拦截,如果用户访问没被拦截的请求,该拦截器不会生效,则token令牌不能进行更新,当用户长时间访问不需要登录的页面,token令牌失效,再去访问被拦截的请求,则需要重新登录,这是不合理的。所有我们还需要在定义一个拦截器,进行token令牌刷新。

  • 刷新令牌的Interceptor

    java 复制代码
    /**
     * 刷新令牌的拦截器
     * @author liang
     */
    public class RefreshTokenInterceptor implements HandlerInterceptor {
    
        private StringRedisTemplate redisTemplate;
    
        public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //从请求头中获取token
            String token = request.getHeader("authorization");
            if (StrUtil.isBlank(token)){
                return false;
            }
            String uuid = token.substring(0, token.lastIndexOf("-"));
            //从Redis中获取值
            Map<Object, Object> userMap = redisTemplate.opsForHash().entries(LOGIN_USER_KEY + uuid);
            if (ObjectUtil.isNull(userMap)){
                return false;
            }
            //将map转换为UserDTO对象
            UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
            //将用户信息保存到 ThreadLocal
            UserHolder.saveUser(userDTO);
            //刷新token有效期
            redisTemplate.expire(LOGIN_USER_KEY + uuid, LOGIN_USER_TTL, TimeUnit.MINUTES);
            return true;
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    }
  • 修改登录的Interceptor

    java 复制代码
    public class LoginInterceptor implements HandlerInterceptor {
    
        /**
         * preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            UserDTO user = UserHolder.getUser();
            return ObjectUtil.isNotNull(user);
        }
    }    
  • 修改WebMvcConfigurer配置类

    java 复制代码
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Resource
        StringRedisTemplate stringRedisTemplate;
    
        /**
         * 添加拦截器
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //添加拦截器
            registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate));
            registry.addInterceptor(new LoginInterceptor())
                    //放行资源
                    .excludePathPatterns(
                            "/shop/**",
                            "/voucher/**",
                            "/shop-type/**",
                            "/upload/**",
                            "/blog/hot",
                            "/user/code",
                            "/user/login"
                    )
                    // 设置拦截器优先级
                    .order(1);
        }
    }

本文由mdnice多平台发布

相关推荐
qq_4419960521 分钟前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼28 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring