Redis(2)短信验证码登录

redis指令参考:https://blog.csdn.net/weixin_43994244/article/details/127489311
redis安装:https://blog.csdn.net/weixin_43994244/article/details/123111671?spm=1001.2014.3001.5502
阿里云短信认证参考:https://blog.csdn.net/weixin_46931860/article/details/124512435

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

2.session拷贝问题:
1、每台服务器中都有完整的一份session数据,服务器压力过大
2、session拷贝数据时,可能会出现延迟

3.Redis代替session需要考虑的问题:
1、选择合适的数据结构
2、选择合适的key
3、选择合适的存储粒度

4.Redis的key满足条件:
1、key要具有唯一性
2、key要方便携带

登录流程

1.发送验证码:

1.用户提交手机号,校验手机号是否合法。
2.如果不合法,则要求用户重新输入手机号,如果手机号合法,后台此时生成对应的验证码
3.保存验证码到redis中,手机号作为key(只在缓存中使用,数据结构简单)
4.通过短信的方式将验证码发送给用户

 @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    //发送短信验证码并保存验证码
    @Override
    public Result sendCode(String phone) throws ClientException {
        //校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //不符合返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //保存验证码到redis中,设置有效时间
        stringRedisTemplate.opsForValue().set("login:code:"+phone,code,5,TimeUnit.MINUTES);
        //发送短信验证码方法
        log.debug("发送验证码成功,验证码: {}",code);
        return Result.ok();
    }

2.短信验证码登录,注册

1.用户输入验证码和手机号,后台从redis中根据key拿到当前验证码
2.和用户输入的验证码进行校验,不一致,则无法通过校验;一致,则后台根据手机号查询用户
3.如果用户不存在,则为用户创建账号信息,保存到数据库
4.将用户信息保存到redis中,随机串token作为key(返回给前端,不能存储敏感信息),方便后续获得当前登录信息

    /**
     * 登录参数,包含手机号、验证码;或者手机号、密码
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm) {
        String phone = loginForm.getPhone();
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //不符合返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2.校验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get("login:token:" + phone);
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.equals(code)) {
           //3.不一致
            return Result.fail("验证码错误");
        }
        //4.根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //5.判断用户是否存在
        if(user == null){
         //6.不存在,创建新用户并保存
         user = createUserWithPhone(phone);
         save(user);
        }
         //7.保存用户信息到redis中
        //7.1 随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //7.2 将User对象转为UserDto作为Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //对象转map,将long类型转为String
        Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                //对key和value自定义-忽略空值-对字段值修改(参数:字段名,字段值)
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll("login:token:"+token,map);
        //设置有效期
        stringRedisTemplate.expire("login:token:",30,TimeUnit.MINUTES);
        //8 返回到token
        return Result.ok(token);
    }

相关类数据:
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;
}

LoginFormDTO:

@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}

UserDTO:

@Data
@ToString
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

登录信息校验正则表达式:

public abstract class RegexPatterns {
    /**
     * 手机号正则
     */
    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}$";
    /**
     * 邮箱正则
     */
    public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
    /**
     * 密码正则。4~32位的字母、数字、下划线
     */
    public static final String PASSWORD_REGEX = "^\\w{4,32}$";
    /**
     * 验证码正则, 6位数字或字母
     */
    public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";

}

校验工具类:

public class RegexUtils {
    /**
     * 是否是无效手机格式
     * @param phone 要校验的手机号
     * @return true:符合,false:不符合
     */
    public static boolean isPhoneInvalid(String phone){
        return mismatch(phone, RegexPatterns.PHONE_REGEX);
    }
    /**
     * 是否是无效邮箱格式
     * @param email 要校验的邮箱
     * @return true:符合,false:不符合
     */
    public static boolean isEmailInvalid(String email){
        return mismatch(email, RegexPatterns.EMAIL_REGEX);
    }

    /**
     * 是否是无效验证码格式
     * @param code 要校验的验证码
     * @return true:符合,false:不符合
     */
    public static boolean isCodeInvalid(String code){
        return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
    }

    // 校验是否不符合正则格式
    private static boolean mismatch(String str, String regex){
        if (StrUtil.isBlank(str)) {
            return true;
        }
        return !str.matches(regex);
    }
}

3.校验登录状态

用户在请求时候,会从携带token到后台,后台通过token从redis中拿到用户信息,如果没有用户信息,则进行拦截,如果有用户信息,则将用户信息保存到threadLocal中,并且放行。
备注:threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离。

1.编写拦截器

public class LoginInterceptor  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();
    }
}

2.配置拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //添加登录拦截器,设置拦截路径
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //登录拦截器
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
        //配置拦截路径
              .excludePathPatterns(
                      "/shop/**",
                      "/voucher/**",
                      "/shop-type/**",
                      "/upload/**",
                      "/blog/hot",
                      "/user/code",
                      "/user/login");
        }
}

3.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();
    }
}

解决状态登录刷新问题

问题:上述拦截器只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,此拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,token仍会过期

解决方案:添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

RefreshTokenInterceptor

/**
 * 拦截一切路径,刷新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 {
        String token = request.getHeader("authorization");

        //放行
        if(StrUtil.isBlank(token)){
           return true;
        }
        //2.基于token获取redis中的用户
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
        //3.判断用户是否存在
        if(map.isEmpty()){
        //4.不存在,拦截,返回401状态码
            return true;
        }
        //5.将查询到的数据转为userDto,保存到ThreadLocal中
        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
        UserHolder.saveUser(userDTO);
        //7.刷新token有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,30, TimeUnit.MINUTES);
        //8.放行
        return true;

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }

}

LoginInterceptor

/**
 * 拦截需要登录的路径
 */
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 
        //用户访问一直不需要登录的页面,拦截器不生效,不刷新token有效期
        //则添加一个 RefreshTokenInterceptor拦截一切路径,刷新token时间
        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 {
        UserHolder.removeUser();
    }

}

拦截器配置

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //添加登录拦截器,设置拦截路径
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //默认顺序为添加顺序
        //token刷新拦截器,拦截所有请求,先执行
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);

        //登录拦截器
        registry.addInterceptor(new LoginInterceptor())
        //配置拦截路径
              .excludePathPatterns(
                      "/shop/**",
                      "/voucher/**",
                      "/shop-type/**",
                      "/upload/**",
                      "/blog/hot",
                      "/user/code",
                      "/user/login");
        }
}
  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值