1.Redis实战—短信登录

短信登录:

基于Redis缓存:

image-20220415100630648


(1)发送短信验证码:

实现逻辑 :

  1. 先校验手机号 ,
    1. 不符合
      1. 直接返回错误信息 , 使用的是封装的返回前端的方法
    2. 符合 ,
      1. 生成一个随机验证码 , 使用的是huTool中的RandomUtil工具类 , 生成随机的六位验证码
      2. 保存验证码到Redis中 , 使用手机号加前缀作为key值保存 , 保证key值唯一性 , 同时设置验证码有效时间
      3. 返回前端数据

代码实现 :

@Override
public Result sendCode(String phone) {
    //TODO 1.校验手机号:不符合是true
    if (RegexUtils.isPhoneInvalid(phone)) {
        //2.如果不符合 , 返回错误信息
        return Result.fail("手机号格式错误");
    }
    // TODO 3.符合 , 生成一个随机验证码 , 使用的是huTool中的工具类 ,
    String code = RandomUtil.randomNumbers(6);
    // TODO 4.保存验证码到Redis当中 , 使用手机号加前缀作为key来保存 , 保证可以的唯一性 , 同时 , 设置有效期为两分钟
    stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
    //5.模拟发送验证码
    log.debug("发送短信验证码成功, 验证码:{" + code + "}");
    //6.返回前端数据ok
    return Result.ok();
}

(2)短信验证码登录注册:

实现逻辑 :

  1. 校验手机号 ,
    1. 不符合
      1. 就直接返回错误信息 , 避免有人使用正确手机号获取验证码 , 但是注册时切换错误手机号
    2. 符合
      1. 从Redis中获取验证码 ,
        1. 判断验证码是否失效(也就是验证码查询不出来) 或者 校验用户输入验证码和Redis中的验证码是否一致
          1. 不符合
            1. 直接输出错误信息
          2. 符合
            1. 从数据库中根据手机号查询用户信息
              1. 查询不出来
                1. 创建新用户 , 使用随机的字符串加前缀作为用户名
              2. 查询出来
            2. 将获取的用户信息选择拷贝搭配DTO类中 , (隐藏用户隐私信息)
            3. 将UserDTO类转换为map类型数据 ,
            4. 设置一个随机字符串和前缀作为该用户的登录令牌
            5. 将令牌作为key , UserDTO作为key进行缓存 , 使用map格式作为缓存数据类型
            6. 将获取的UserDTO返回前端

代码实现 :

@Override
public Result login(LoginFormDTO loginForm) {
    //1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式错误");
    }

    //2. TODO 校验验证码 , 从redis中获取验证码
    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        //3.判断验证码是否失效(也就是验证码查询不出来) 或者 校验用户输入验证码和Redis中的验证码是否一致
        return Result.fail("验证码错误");
        // TODO 使用反证的方式 , 可以减少if语句的判断次数
    }
    //4.一致 , 根据手机号查询用户

    // TODO 使用的是MyBatisPlus中的方法 , 进行查询的
    User user = query().eq("phone", phone).one();
    //5.判断用户是否存在
    if (user == null) {
        //6.不存在 , 创建新用户并保存
        user = createUserWithPhone(phone);
    }

    // TODO 7.保存用户信息到redis中
    // TODO 7.1 随机生成token作为登录令牌
    
    // TODO 使用huTool提供的UUID , 下边的写法是生成不带下划线的UUID , 默认值为false,带下划线的UUID
    String token = UUID.randomUUID().toString(true);

    // TODO 使用BeanUtil中的copyProperties方法 , 可以将user中的属性自动拷贝到UserDTO中 , 对于没有的属性,不进行拷贝
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

    // TODO 7.2 将User对象转为Hash集合 , 将UserDTO转换为一个Map集合 ,
    //  这个时候 , 进行转换的时候 ,会出现异常 , 因为我们使用的是String类型的redis对象 , 在转换的时候 , key值只能是String类型的
    //  但是 , 这个BeanUtil工具类 , 允许我们进行自定义 ,
    //  添加两个参数 , 一个是new HashMap<>() ,
    //  一个是CopyOptions , 定义自定义的操作
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true) // TODO 设置是否忽略空值
                    // TODO 对字段值的修改器 , 需要两个参数 , 修改前的字段名和字段值 , 修改后的字段值
                    .setFieldValueEditor((fileName,fileValue) -> fileValue.toString()));

    // TODO 7.3 存储
    String tokenKey = LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
    // TODO 7.4 设置token的有效期 , redis中不能在上一个方法中直接设置有效期 , 可以在下边设置
    stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
    // TODO 8.返回token , 用来将这个token保存在浏览器中 , 下次登录的时候 , 会携带这个token进行访问
    return Result.ok(token);
}

//TODO 创建用户
private User createUserWithPhone(String phone) {
    //1.创建用户 :
    User user = new User();
    user.setPhone(phone);
    //生成随机的字符串 , 用来当做用户名
    user.setNickName("user_" + RandomUtil.randomString(10));
    save(user);
    return user;
}

(3)拦截是否有登录令牌 , 有就刷新 , 没有就直接放行

  • 使用拦截器 , 负责检测用户登录的时间 , 使用缓存 token来定义用户的登录时间 , 只要有操作 , 就刷新token的时间 ,

  • 只负责检测请求中有没有携带token , 没有就直接放行 , 有了就刷新token ,


实现逻辑 :

  1. 设置前置拦截 , 获取请求头中的token
    1. 为空 , 说明没有登录 , 使用isBlank判断 ,
      1. 直接放行
    2. 不为空 , 获取Redis中的用户的信息
      1. 判断获取的map集合是否为空
        1. 为空 , 直接放行
        2. 不为空 , 将获取到的信息转为Map格式 , 存储在ThreadLocal域中
        3. 刷新token的有效期

代码实现 :

/**
 * 拦截器类 , 负责刷新token保存时间的 , 只有用户登录了才进行操作 , 其他的一概放行
 */
public class RefreshTokenInterceptor implements HandlerInterceptor {

    // TODO 注意: 拦截器是我们自己创建的类 , 不受Spring容器管理 , 所以 , 不能直接注入RedisTemplate
    // TODO 我们只能使用构造函数的方式进行注入 , 谁调用这个拦截器 ,谁负责注入这个RedisTemplate
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    //前置拦截
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // TODO 1.获取请求头中的token
        String token = request.getHeader("authorization");
        // TODO StrUtil中的isBlank方法就是判断是否是空值
        if (StrUtil.isBlank(token)){
            // TODO 为空,说明没有登录 , 直接放行
            return true;
        }
        // TODO 2.基于token获取redis中的用户信息
        // TODO 不能简单的使用get来获取值了 , 使用get获取的只是hash中的map中的一个值 , 而我们想获取的是全部的值 , 使用entries这个方法
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        // TODO 不用判断是否为null了,entries会做判断 , 如果为null会返回一个空的map , 所以这里只用判断是否为空就可以了
        if (userMap.isEmpty()){
            // TODO 为空说明用户没有登录, 直接放行 , 不作操作
            return true;
        }
        // TODO 不为空 , 进行token以及数据的保存工作
        // TODO 5.将查询到的Hash数据转为UserDTO对象 , 最后一个参数是否忽略转换中的异常 , false是不忽略
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // TODO 5.存在 , 保存用户信息到ThreadLocal , 这是一个工具类 , 内部创建了一个ThreadLocal对象,来进行操作
        UserHolder.saveUser(userDTO);

        // TODO 7.刷新token的有效期 , 也就是从新设置对应key的有效时间
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+ token,LOGIN_USER_TTL, TimeUnit.MINUTES);
        //6.放行
        return true;
    }

    @Override
    //后置拦截
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //删除用户
        UserHolder.removeUser();
    }

(4)拦截是否登录 :

有了第一个拦截器 , 这个类 就只用拦截是否登录即可 ,

实现逻辑 :

  1. 从ThreadLocal域中获取用户信息 .
    1. 只要为null , 就说明没有登录 , 直接拦截
    2. 不为null , 放行

代码实现 :

/**
 * 拦截器类 , 负责拦截是否登录的
 */
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    //前置拦截
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // TODO 1.判断是否需要拦截 , ThreadLocal中是否有用户信息
        if (UserHolder.getUser() == null){
            // 没有 , 需要拦截 , 设置状态码 ,
            response.setStatus(401);
            // 拦截
            return false;
        }
        //有用户 , 直接放行
        return true;
    }
}

(5)拦截器的配置类:

@Configuration
// TODO 配置拦截器
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // TODO token 刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                .addPathPatterns("/**")
                .order(0); // TODO 设置优先级 ,值越小 , 优先级越高

        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                "/blog/hot",
                "/shop-type/**",
                "/upload/**",
                "/voucher/**",
                "/shop/**",
                "/user/code",
                "/user/login"
        ).order(1);
    }
}
     .excludePathPatterns(
            "/blog/hot",
            "/shop-type/**",
            "/upload/**",
            "/voucher/**",
            "/shop/**",
            "/user/code",
            "/user/login"
    ).order(1);
}

}


---
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值