黑马点评-项目集成git及redis实现短信验证码登录

news2025/1/16 8:17:34

目录

IDEA集成git

传统session存在的问题 

redis方案

业务流程

选用的数据结构

整体访问流程

发送短信验证码

 获取校验验证码

配置登录拦截器

拦截器注册配置类

拦截器

用户状态刷新问题

刷新问题解决方案


IDEA集成git

远程仓库采用码云,创建好仓库,复制仓库的url

 在idea中点击,出现git选项,点击ok

 之后右击项目,点击remotes

 填写url即可集成git

传统session存在的问题 

传统的登录认证会采用session进行登录认证,将登录的验证码,用户信息都存放到session中,我们通过session来进行操作数据,这有什么问题呢

每个tomcat服务器中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,即session在各个服务器之间是不共通的,所以此时整个登录拦截功能就会出现问题,而redis数据本身就是共享的,就可以避免session共享的问题了

redis方案

业务流程

  • 将验证码存储到redis中
  • 将用户数据存储到redis中

选用的数据结构

存储验证码时采用String类型,key采用业务代码+手机号

存储user数据时采用hash,key采用业务代码+随机的tonke

hash可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少,实际上也可以采用String类型,但hash类型消耗内存较少,故选用String类型

整体访问流程

 当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,简化后续业务获取用户信息的代码,后续业务需要登录用户的信息,只要在threadLocal中取即可,无需从redis中取从而增加复杂度,最后放行。

redis业务代码常量(后续继续补充)

public class RedisConstants {
    //发送验证码业务标识
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    //用户登录业务标识
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;

   
}

发送短信验证码

采用日志打印形式,将验证码打印在控制台

 public Result sendCode(String phone) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号码格式不正确");
        }
        //手机号格式正确生成验证码
        String code = RandomUtil.randomNumbers(6);
        //保存到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //打印日志
        log.debug("手机验证码为:" + code);
        return Result.ok();
    }

 获取校验验证码

我们需要对用户敏感信息进行筛选,只给前端返回必要的用户信息数据,需要封装dto对象,同时在uuid生成token,作为redis取数据的key,最后要设置过期时间,防止reids内存爆炸,设置为30分钟,因为session的过期时间也是30分钟

 public Result login(LoginFormDTO loginForm) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号码格式不正确");
        }
        //从redis取验证码,进行比对
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (cacheCode == null || !cacheCode.equals(code)) {
            return Result.fail("验证码错误");
        }
        //根据电话号码从数据库查询用户是否存在
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone, phone);
        User user = userMapper.selectOne(queryWrapper);
        //不存在则创建
        if (user == null) {
            user = createUserWithPhone(phone);
        }
        String token = UUID.randomUUID().toString(true);
        //只返回不敏感的信息,使用dto进行封装
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user, userDTO);
        //将dto对象转化为map结构
        Map<String, String> userMap = new HashMap<>();
        userMap.put("id", userDTO.getId().toString());
        userMap.put("icon", userDTO.getIcon());
        userMap.put("nickName", userDTO.getNickName());
//业务代码+token形成redis中的key
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        return Result.ok(token);
    }

配置登录拦截器

拦截器注册配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {
  @Autowired
    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"
                );
    }
}

拦截器

拦截器需要的stringRedisTemplate对象不能通过@Autowired直接注入

因为LoginInterceptor 对象是我们手动创建的,不受spring管控,不在spring容器中,故不能注入spring容器中的bean,只能通过在配置类中通过构造方法注入stringRedisTemplate对象

public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(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)) {
            response.setStatus(401);
            return false;
        }
        String tokenKey = LOGIN_USER_KEY + token;

        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
        //3.判断用户是否存在
        if (userMap.isEmpty()) {
            //4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //5.存在,保存用户信息到Threadlocal
        UserHolder.saveUser(userDTO);
        //刷新登录状态
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //6.放行
        return true;
    }
}

用户状态刷新问题

那就是登录认证和刷新用户状态绑定在一个拦截器中,而需要登录认证的路径并不是全部路径,如果用户不访问需要登录认证的路径,那就刷新不了用户状态,即30分钟之后登录就会退出,这明显不符合我们的常识

刷新问题解决方案

我们可以再加一个拦截器,形成一个拦截器链,第一个拦截器对所有路径进行拦截,而它的功能就是刷新登录状态,登录认证则由第二个拦截器完成

 

 刷新状态拦截器

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 {
        // 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();
    }

登录认证拦截器

public class LoginInterceptor implements HandlerInterceptor {

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

为了保证拦截器的执行先后顺序,配置类需设置order的大小,设定拦截器执行的先后的顺序,如若不设置,就按照注册顺序的先后来执行

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/896743.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【STM32】FreeRTOS软件定时器学习

软件定时器 FreeRTOS提供了现成的软件定时器功能&#xff0c;可以一定程度上替代硬件定时器&#xff0c;但精度不高。 实验&#xff1a;创建一个任务&#xff0c;两个定时器&#xff0c;按键开启定时器&#xff0c;一个500ms打印一次&#xff0c;一个1000ms打印一次。 实现&…

C++快速回顾(一)

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》&#xff0c;结合我自己的工作学习经历&#xff0c;我准备写一个音视频系列blog。C/C是音视频必…

大型集团企业数字化管控平台及信息化治理服务体系建设方案PPT

导读&#xff1a;原文《大型集团企业数字化管控平台及信息化治理服务体系建设方案PPT》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 喜欢文章&#xff0c;您可以点…

B站视频码率用户上传视频的视频码率

一般来说&#xff0c;B站用户可以根据自己的视频内容和需求来选择视频的码率&#xff0c;但以下是一些常见的视频码率范围&#xff0c;供用户参考&#xff1a; 标清&#xff08;SD&#xff09;&#xff1a; 码率范围可能在500 Kbps至1.5 Mbps左右&#xff0c;适用于480p的分辨率…

Vue的鼠标键盘事件

Vue的鼠标键盘事件 原生 鼠标事件(将v-on简写为) click // 点击 dblclick // 双击 mousedown // 按下 mousemove // 移动 mouseleave // 离开 mouseout // 移出 mouseenter // 进入 mouseover // 鼠标悬浮mousedown.left 键盘事件 keydown //键盘按下时触发 keypress …

Kick Back Relax:Learning to Reconstruct the World by Watching SlowTV

参考代码&#xff1a;slowtv_monodepth 这篇文章提出了一种在无标定数据集上自监督估计深度的方法&#xff0c;也就是通过网络预测的方式估计相机的内参&#xff0c;从而完成自监督流程。为了验证在无相机标定情况下实现自监督深度估计&#xff0c;从网络上下载了一些视频构建…

板卡常用前端 数据表操作

两年前写的&#xff0c;现在看,有点想吐, 数据操作表,调试设备用 采用外挂的方法&#xff0c;以前设备的接口命令,简易&#xff0c;换个UI展示很容易 自己写着玩的,公司部分产品再用,前端展示,不涉密 index.html <!doctype html> <html><head><meta chars…

vue根据template结构自动生成css/scss/less样式嵌套

vscode搜索安装插件&#xff1a;AutoScssStruct4Vue

Java实现postgre数据库每日定时自动备份

前提&#xff1a;该备份仅为同数据库不同schema备份 假设需要备份的数据库为test&#xff0c;schema为public。代码如下 public void backupAllTables() {log.info("备份全表开始执行" System.currentTimeMillis());String origScheme1 "public";String…

卷积神经网络全解!CNN结构、训练与优化全维度介绍!

目录 一、引言1.1 背景和重要性1.2 卷积神经网络概述 二、卷积神经网络层介绍2.1 卷积操作卷积核与特征映射卷积核大小多通道卷积 步长与填充步长填充 空洞卷积&#xff08;Dilated Convolution&#xff09;分组卷积&#xff08;Grouped Convolution&#xff09; 2.2 激活函数R…

Excel自动化办公——Openpyxl的基本使用

Excel自动化办公——Openpyxl的基本使用 个人感觉&#xff0c;相比Pandas&#xff0c;openpyxl对Excel的操作更为细致&#xff0c;Pandas则更适用于统计计算&#xff1b; 01 基本环境02 Excel数据读取操作03 案例04 向Excel写入数据05 表数据定向修改06 单元格样式制定07 单元…

2019年3月全国计算机等级考试真题(C语言二级)

2019年3月全国计算机等级考试真题&#xff08;C语言二级&#xff09; 第1题 负责数据库中查询操作的数据库语言是 A. 数据定义语言 B. 数据管理语言 C. 数据操纵语言 D. 数据控制语言 正确答案&#xff1a;C 第2题 有关系如下图所示&#xff0c;其违反了哪一类完整性约束 …

c++——静态成员变量、静态成员函数和const修饰的静态成员变量

一、c静态成员变量 在C中&#xff0c;静态成员变量&#xff08;Static Member Variable&#xff09;是类的一种特殊类型的成员变量&#xff0c;它被类的所有实例共享&#xff0c;而不是每个实例都有自己的副本。静态成员变量在类的所有对象之间保持唯一的状态&#xff0c;具有…

ElementPlusError 解决: [ElForm] model is required for validate to work 的三种方式

1、问题描述&#xff1a; 其一、报错为&#xff1a; ElementPlusError: [ElForm] model is required for validate to work. 中文为&#xff1a; ElementPlusError&#xff1a;验证工作需要 [ElForm] 模型 其二、问题描述为&#xff1a; 在 form 表单中需要进行规则校验&a…

分布式锁系列之Redis分布式锁

目录 介绍 模拟订单超卖场景 代码版 不加锁情况 synchronized加锁 ​编辑 lock加锁 整合Mysql版 不加锁版 synchronized加锁 lock加锁版 jvm加锁失效情况 多例模式 事务 集群搭建 书写sql解决集群超卖 使用悲观锁select ...for update ​编辑 不加悲观锁情况 使用…

备份或同步数据?跨国大文件传输的不同需求与解决方案

信息化时代的到来&#xff0c;使得许多个人、组织、企业在日常生活中都需要对数据进行备份或同步。但不同的应用场景和需求&#xff0c;也需要不同的备份和同步方式。而在跨国大文件传输方面&#xff0c;更是需要根据不同需求选择合适的传输方案。下面将分别介绍备份与同步数据…

第一章MyBatis概述

框架 Java常用框架 SSM三大框架&#xff1a;SpringSpringMVCMyBatisSpringBootSpringCloud 框架简单来说就是提前封装好的通用代码&#xff0c;提高我们的开发的效率&#xff08;站在巨人的肩膀上&#xff09; 框架一般以jar包的形式存在 三层架构 代码封装主要为了降低代…

MQTT 常用客户端库介绍 (全面涵盖c,c++,java,c#,python)

MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的通信协议&#xff0c;被广泛应用于物联网和分布式系统中。它以其简单、可靠和高效的特性而备受推崇&#xff0c;成为连接设备和应用程序的首选协议。MQTT的重要性不言而喻&#xff0c;它为实时通…

K8S应用笔记 —— 签发自签名证书用于Ingress的https配置

一、需求描述 在本地签发自命名证书&#xff0c;用于K8S集群的Ingress的https配置。 前提条件&#xff1a; 完成K8S集群搭建。完成证书制作机器的openssl服务安装。 二、自签名证书制作 2.1 脚本及配置文件准备 2.1.1 CA.sh脚本准备 注意事项&#xff1a; openssl服务默认CA…

【vim 学习系列文章 5 - cscope 过滤掉某些目录】

文章目录 cscope 过滤目录介绍 cscope 过滤目录介绍 第一步创建自己的cscope脚本~/.local/bin/cscope.sh&#xff0c;如下&#xff1a; function my_cscope() {CODE_PATHpwdecho "$CODE_PATH"echo "start cscope...."if [ ! -f "$CODE_PATH/cscope.…