Redis实现短信验证码登录

本文详细介绍了如何使用Redis来存储短信验证码进行登录验证,包括手机号格式检查、验证码生成与存储、用户信息的hash存储。同时,文中还阐述了利用拦截器A和B分别刷新令牌有效期和限制未登录访问,确保用户会话安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Redis实现短信验证码登录

要保存验证码到Redis中,可以直接使用String类型进行存储,手机号作为key,value存储验证码

用户信息则使用hash进行存储,使用随机token为key存储用户数据

首先,明确验证码登录的流程

  • 发送验证码

    1. 首先要验证用户输入的手机号是否符合格式

      /**
           * 手机号正则
           */
          public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
      
    2. 符合则生成验证码

    3. 将生成的验证码存储到Redis,key设置为login:code:phone的格式,并且设置有效期为2分钟

    4. 发送验证码

    5. 返回生成成功

    public Result sendCode(String phone, HttpSession session) {
        //1.检验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.不符合返回错误信息
            return Result.fail("手机号格式错误");
        }
        //3.符合生成验证码
        String code = RandomUtil.randomNumbers(6);
    
        //4.保存验证码到Redis,并且设置有效期2分钟
        redisTemplate.opsForValue().set(LOGIN_CODE_KEY+ phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
    
        //5.发送验证码(模拟发送)
        log.debug("发送验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }
    

    在Redis中存储验证码

  • 登录方法验证验证码

    1. 检验手机号格式
    2. 验证传入的验证码是否和redis中存储的验证码相同
    3. 查看用户是否存在,如果没有注册直接进行注册,如果存在则直接登录
    4. UUID随机生成作为登录令牌
    5. 将User对象转为存储基本信息的UserDTO
    6. 将UserDTO转为Hash存储在Redis中
    7. 设置有效期,如果半小时内用户不再访问网页则令牌过期(借助拦截器实现,如果用户在访问别的页面,那么必然经过拦截器,此时就为用户更新令牌的过期时间)
    8. 返回token给前端

    首先,将登录方法逻辑完成

    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //检验手机号格式
        if(RegexUtils.isPhoneInvalid(loginForm.getPhone()))
        {
            return Result.fail("手机号格式错误!");
        }
        //验证验证码
        String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY+ loginForm.getPhone());
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.equals(code))
        {
            return Result.fail("验证码错误");
        }
        //查看手机是否已注册
        User user = query().eq("phone", loginForm.getPhone()).one();
        //用户是否存在,不存在直接注册,存在则登录
        if(user == null){
            user = createUserWithPhone(loginForm.getPhone());
        }
    
    
        //随机生成token作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //将User对象转为Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true)
                .setFieldValueEditor((fieldName,filedValue)->filedValue.toString()));
        redisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
        String tokenKey = LOGIN_USER_KEY + token;
        //设置有效期
    
        redisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.SECONDS);
    
        //返回token
        return Result.ok(token);
    }
    private User createUserWithPhone(String phone){
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        save(user);
        return user;
    }
    

    登录成功则存储令牌信息到Redis

在这里插入图片描述

编写工具类存储UserDTO到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();
    }
}

编写两个拦截器A、B,分别具备不同功能,用户的请求会先经过拦截器A再经过拦截器B。

  • 拦截器A:A拦截一切路径,但是直接放行。目的是在A拦截器中刷新用户的令牌有效期,让用户在使用过程中令牌有效期始终为30min。拦截器A需要:

    • 获取token
    • 查询Redis中是否有该用户,有则获取用户信息
    • 保存用户到ThreadLocal
    • 刷新token有效期
    • 放行
    public class RefreshTokenInterceptor implements HandlerInterceptor {
        //因为这是自己写的类,并不是Spring配置的类,不能直接@Autowired注入SpringRedisTemplate,可以通过MvcController进行注入,在构造方法赋值
        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");
    
            //判断token是否为空
            if(StrUtil.isBlank(token)){
                return true;
            }
    
            //获取Redis中用户
            String key = RedisConstants.LOGIN_USER_KEY + token;
            Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key);
    
            //判断用户是否存在
            if(userMap.isEmpty()){
                return true;
            }
            //将hash数据转换成UserDTO对象
            UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
            //存在则保存用户信息到ThreadLocal
            UserHolder.saveUser(userDTO);
    
            //刷新token有效期
            redisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        }
    }
    
  • 拦截器B:B则拦截需要所有登陆的路径,目的是限制未登录状态下能访问的请求。拦截器B需要:

    • 查询ThreadLocal中的用户
    • 不存在则拦截,存在则放行
    public class LoginInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //判断ThreadLocal中是否有用户是否需要拦截
            if(UserHolder.getUser() == null){
                response.setStatus(401);
                return false;
            }
    
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        }
    }
    

编写MvcConfig配置拦截器

public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate redisTemplate;


    @Override
    public void addInterceptors(InterceptorRegistry registry){
        //order表示优先级,数字越小越先执行,默认都是0,执行顺序按照添加顺序执行
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("" +
                "/user/code","/user/login","/blog/hot","/shop/**"
        ,"/shop-type/**","/upload/**","/voucher/**").order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate)).addPathPatterns("/**").order(0);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值