当前位置: 首页 > article >正文

Redis项目:短信验证码登录

这是黑马的黑马点评项目,短信验证码的业务。一开始是使用session做的,后来重构,使用redis缓存来完成。

第一层拦截器:

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 (StrUtil.isBlank(token)) {
            return true;
        }
        // 基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        //判断用户是否存在
        // 不存在,拦截,返回401状态码
        if (userMap.isEmpty()) {
            return true;
        }
        // 查询到的hash数据转换成UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 存在 保存用户信息到Threadlocal
        UserHolder.saveUser(userDTO);
        // 刷新token有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //放行
        return true;
    }

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

第一次拦截器的逻辑:

  • preHandle 方法
    1. 从请求头中获取 authorization token。
    2. 如果 token 为空,直接放行(返回 true)。
    3. 根据 token 从 Redis 中查询用户信息(存储在 Hash 结构中)。
    4. 如果用户信息不存在,直接放行(返回 true)。
    5. 如果用户信息存在:
      • 将 Redis 查询到的 Hash 数据转换为 UserDTO 对象。
      • 将用户信息保存到 UserHolder(ThreadLocal)。
      • 刷新 Redis 中 token 的有效期。
      • 返回 true 放行请求。
  • afterCompletion 方法
    • 在请求处理完成后,移除 UserHolder 中的用户信息,清理线程局部变量

在authorization 中它记录的是客户端发送的身份验证凭证

刷新 Redis 中 token 的有效期是通过调用 Redis 的 EXPIRE 命令完成的

在转成对象的时候 参数false:一个布尔参数,通常用于控制填充行为.如果 Map 中的键无法匹配目标对象的属性,或者类型转换失败,会抛出异常(例如 BeanException),填充过程中止

第二次拦截器

第二次拦截就只需要判断线程里有没有用户了

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //这个拦截器只需要判断是不是需要拦截(ThreadLocal中有没有用户)
        if (UserHolder.getUser() == null) {
            //没有用户信息 拦截 返回401
            response.setStatus(UNAUTHORIZED_401);
            return false;
        }
        //有用户 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        UserHolder.removeUser();
    }
}
  • preHandle
    • 检查 UserHolder(ThreadLocal)中是否有用户信息。
    • 如果没有(UserHolder.getUser() == null),设置响应状态码为 401(未授权)并拦截请求(返回 false)。
    • 如果有用户信息,放行请求(返回 true)。
  • afterCompletion
    • 在请求处理完成后,清理 UserHolder 中的用户信息。
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();
    }
}

 ThreadLocal 是 Java 中的一种线程隔离机制,用于在多线程环境中为每个线程提供独立的变量副本

每个线程访问 ThreadLocal 时,获取的是自己线程的独立副本,互不干扰.

ThreadLocal 中的数据与线程绑定,线程结束时数据自动销毁(如果正确清理)

  • ThreadLocal 维护一个 ThreadLocalMap,存储在每个 Thread 对象中。
  • set 方法将值存入当前线程的 ThreadLocalMap,键是 ThreadLocal 对象,值是用户设置的数据。
  • get 方法从当前线程的 ThreadLocalMap 中取值。
  • remove 方法删除当前线程的 ThreadLocalMap 中的对应键值对

发送验证码

@Override
    public Result sendCode(String phone, HttpSession session) {
        //1、校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //2、如果不符合,返回错误信息
            return Result.fail(cacheConstants.ERROR_USER_PHONE);
        }
        //3、符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4、保存验证码 把验证码存入redis中
        //session.setAttribute(cacheConstants.USER_LOGIN_CAPTCHA_KEY, code);
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //5、发送验证码
        log.info("验证码发送成功{}", code);
        //6、返回结果对象
        return Result.ok();
    }

 验证码被存在了redis中,而不是session中

校验验证码

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            //如果不符合,返回错误信息
            return Result.fail(cacheConstants.ERROR_USER_PHONE);
        }
        //校验验证码 从redis中获取
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        //验证码不一致 报错
        if (cacheCode == null || !cacheCode.equals(code))
            return Result.fail(cacheConstants.ERROR_USER_CAPTCHA_KEY);
        //验证码一致 查找用户 SELECT * FROM tb_user where phone = ?
        User user = query().eq(cacheConstants.USER_LOGIN_PHONE, phone).one();
        // 查询用户 是否存在 不存在 创建用户
        if (user == null)
            user = createUserWithPhone(phone);
        //保存用户信息到redis中
        //生成token作为key
        String token = UUID.randomUUID().toString();
        //将User对象转为hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        String tokenKey = LOGIN_USER_KEY + token;
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        //存储 这里注意 user中id属性是long 类型 需要的是string类型 利用putAll参数进行转换
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        //设置有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        //返回token
        return Result.ok(token);
    }

    private User createUserWithPhone(String phone) {
        //创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(cacheConstants.USER_LOGIN_USER + RandomUtil.randomString(10));
        save(user);
        return user;
    }

先是校验一下手机号是不是合法 也可以再校验一下验证码 

然后就是从redis中取得验证码 进行比对

验证码一致就可以从数据库中查找是否有用户

有用户 用户数据会通过mybatisPlus自动映射到对象user上 

没有用户 调用创建用户 

然后会生成一个token,作为key的一部分(客户端在后续请求中携带 token(通常放在 Authorization 请求头中))

将user的部分信息封装到UserDto之中  然后将对象转成map集合的形式

之后以hash的形式存入redis 设置有效期

trips

CopyOptions 配置了一些转换选项 ,CopyOptions.create() 返回一个默认的 CopyOptions 对象,可以通过链式调用设置选项。.setIgnoreNullValue(true),忽略值为 null 的属性。.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())自定义字段值的编辑器,通过 Lambda 表达式处理每个属性值,将每个属性值转换为字符串

用putAll可以一次性把数据全部传进去

疑问:

authorization基于什么生成的?

......


http://www.kler.cn/a/591950.html

相关文章:

  • 【JDK17】开源应用服务器大比对
  • logparser日志分析详解
  • ubuntu20.04安装mysql-workbench
  • DataWhale 大语言模型 - 模型详细配置
  • conda 的 envs_dirs 配置出错
  • 解决 ECharts 切换图表时的 Resize 问题
  • 博客图床 VsCode + PicGo + 阿里云OSS
  • SQLark中如何进行数据筛选与排序
  • 批量测试IP和域名联通性2
  • Seaborn 数据可视化指南:核心功能与实战技巧
  • Android wifi的开关Settings值异常分析
  • Type-C:智能家居的电力革命与空间美学重构
  • 前端vue3 setup,后端fastapi
  • 09.【C++】list链表(STL中的列表容器,C++封装的带头双向链表,可实现指定类型的增删查改,迭代器操作等功能)
  • Qt 中工具窗体与普通窗体在任务栏中的区别
  • 基于微信小程序的网上商城
  • jmeter-sample
  • MySQL日期转字符串,字符串转日期的函数
  • Skia 图形引擎介绍
  • Vim软件使用技巧