Loading

使用Redis实现短信登陆

使用Redis实现发送验证码;验证码登陆、注册;登陆校验拦截、登陆状态刷新等一系列问题。

验证码发送和验证登陆注册

思路流程

整体的思路以及流程如题:

代码实现

  1. 实体类

    User实体类

    @Data
    @EqualsAndHashCode(callSuper = false)
    @Accessors(chain = true)
    @TableName("tb_user")
    public class User implements Serializable {
    
      private static final long serialVersionUID = 1L;
      
          @TableId(value = "id", type = IdType.AUTO)
          private Long id;
          private String phone;
          private String password;
          private String nickName;
          private String icon = "";
          private LocalDateTime createTime;
          private LocalDateTime updateTime;
    }
    

    UserDTO

    import lombok.Data;
    
    @Data
    public class UserDTO { 
        private Long id;
        private String nickName;
        private String icon;
    }
    
  2. 发送手机验证码

    import cn.hutool.core.util.RandomUtil;
    
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    
    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone) {
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        // 符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        String key = LOGIN_CODE_KEY + phone;
      
        // 保存验证码, redis
        stringRedisTemplate.opsForValue().set(key, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 发送验证码,模拟
        log.debug("发送短信验证码成功,验证码:{}", code);
        return Result.ok();
    }
    
  3. 短信验证登陆注册

    import cn.hutool.core.lang.UUID;
    
    @Resource
    private IUserService userService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;
    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm){
        String phone = loginForm.getPhone();
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        // 校验验证码
        String key = LOGIN_CODE_KEY + phone;
        String cacheCode = stringRedisTemplate.opsForValue().get(key);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
          	// 不一致,返回错误信息
            return Result.fail("验证码错误");
        }
        // 一致,根据手机号查询用户
        User user = userService.queryUser(phone);
        if (user == null) {
            // 不存在,创建新用户并保存
            user = createUserWithPhone(phone);
            userService.save(user);
        }
        // 保存用户信息到 redis
        String token = UUID.randomUUID().toString(true);
        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()
                ));
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 返回token
      	return Result.ok(token);
    }
    
    /**
     * 创建用户
     * @param phone	手机号
     */
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        return user;
    }
    

登陆校验拦截

思路流程

  • 第一个拦截器是为了刷新token的有效期,防止用户在访问不需要登陆操作页面时间过长而导致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 {
            // 获取请求头中的token
            String token = request.getHeader("authorization");
            if (StringUtils.isBlank(token)) {
                return true;
            }
            // 获取redis中的用户
            String key = RedisConstants.LOGIN_USER_KEY + token;
            Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(key);
    
            if (entries.isEmpty()) {
                return true;
            }
            UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
            // 存在,保存到ThreadLocal
            UserHolder.saveUser(userDTO);
            // 刷新token有效期
            stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserHolder.removeUser();
        }
    
  • 第二个拦截器

    public class LoginInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            if (UserHolder.getUser() == null) {
                // 不存在,拦截,返回401状态码
                response.setStatus(401);
                return false;
            }
            return true;
        }
    }
    
  • 注册拦截器

    注意拦截顺序,默认按照添加顺序,也可手动指定。

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
            registry.addInterceptor(new LoginInterceptor())
                    .excludePathPatterns(
                            "/voucher/**",
                            "/shop-type/**",
                            "/shop/**",
                            "/user/code",
                            "/user/login",
                            "/blog/hot"
                    ).order(1);
        }
    }
    




上一篇文章:Redis快速入门
下一篇文章:

posted @ 2022-10-26 15:42  sw-code  阅读(204)  评论(0编辑  收藏  举报