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

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

目录

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);//保存到redisstringRedisTemplate.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中的keyString 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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic 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;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString 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.存在,保存用户信息到ThreadlocalUserHolder.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;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString 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数据转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}

登录认证拦截器

public class LoginInterceptor implements HandlerInterceptor {@Overridepublic 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 {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic 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);}
}

相关文章:

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

目录 IDEA集成git 传统session存在的问题 redis方案 业务流程 选用的数据结构 整体访问流程 发送短信验证码 获取校验验证码 配置登录拦截器 拦截器注册配置类 拦截器 用户状态刷新问题 刷新问题解决方案 IDEA集成git 远程仓库采用码云&#xff0c;创建好仓库&…...

mac苹果电脑怎么运行Windows软件?怎么安装Win虚拟机?

近年来&#xff0c;苹果电脑的用户群体不断扩大&#xff0c;许多用户对于苹果电脑是否可以运行Windows软件产生了疑问。苹果电脑和Windows操作系统有着明显的区别&#xff0c;是否能够在苹果电脑上运行Windows软件。下面我们就来看苹果电脑可以运行Windows软件吗&#xff0c;苹…...

Jmeter对websocket进行测试

JMeterWebSocketSampler-1.0.2-SNAPSHOT.jar下载 公司使用websocket比较奇怪&#xff0c;需要带认证信息进行长连接&#xff0c;通过websocket插件是请求失败&#xff0c;如下图&#xff0c;后面通过代码实现随再打包jar包完成websocket测试 本地实现代码如下&#xff1a; pa…...

从2023年世界机器人大会发现机器人新趋势

机器人零部件为何成2023年世界机器人大会关注热门&#xff1f; 在原先&#xff0c;机器人的三大核心零部件是控制系统中的控制器、驱动系统中的伺服电机和机械系统中的精密减速器。如今&#xff0c;机器人的主体框架结构已经落实&#xff0c;更多机器人已经开始深入到各类场景中…...

Kafka单节点部署

&#x1f388; 作者&#xff1a;互联网-小啊宇 &#x1f388; 简介&#xff1a; CSDN 运维领域创作者、阿里云专家博主。目前从事 Kubernetes运维相关工作&#xff0c;擅长Linux系统运维、开源监控软件维护、Kubernetes容器技术、CI/CD持续集成、自动化运维、开源软件部署维护…...

生成式AI和大语言模型 Generative AI LLMs

在“使用大型语言模型(LLMs)的生成性AI”中&#xff0c;您将学习生成性AI的基本工作原理&#xff0c;以及如何在实际应用中部署它。 通过参加这门课程&#xff0c;您将学会&#xff1a; 深入了解生成性AI&#xff0c;描述基于LLM的典型生成性AI生命周期中的关键步骤&#xff…...

Obsidian 入门使用手册

文章目录 一、Obsidian 入门1.1 什么是 Obsidian1.2 安装 Obsidian 二、Obsidian 配置2.1 创建第一个笔记2.2 设置界面语言使用中文2.3 主题 三、小结 一、Obsidian 入门 1.1 什么是 Obsidian Obsidian 是一款基于 Markdown 语法编辑的笔记软件。与传统的 Markdown 软件不同的…...

GuLi商城-前端基础Vue指令-单向绑定双向绑定

什么是指令? 指令 (Directives) 是带有 v- 前缀的特殊特性。 指令特性的预期值是:单个 JavaScript 表达式。 指令的职责是&#xff0c;当表达式的值改变时&#xff0c;将其产生的连带影响&#xff0c;响应式地作用于DOM 例如我们在入门案例中的 v-on&#xff0c;代表绑定事…...

前端(十三)——JavaScript 闭包的奥秘与高级用法探索

&#x1f636;博主&#xff1a;小猫娃来啦 &#x1f636;文章核心&#xff1a;深入理解 JavaScript 中的闭包 文章目录 不理解闭包&#xff1f;这玩意很难&#xff1f;闭包的定义与原理闭包是什么创建一个闭包 闭包的应用场景闭包与作用域闭包与作用域之间的关系全局作用域、函…...

面试-快速学习计算机网络-UDP/TCP

1. OSI四层和七层映射 区别&#xff1a; 应用层&#xff0c;表示层&#xff0c;会话层合并为了应用层数据链路层和物理层合并为了网络接口层 2. TCP和UDP的区别&#xff1f; 总结&#xff1a; 1 . TCP 向上层提供面向连接的可靠服务 &#xff0c;UDP 向上层提供无连接不可靠服…...

爱校对如何帮助企业和博客主提高在线可见性?

在数字化时代&#xff0c;内容质量已经成为增强在线曝光率的关键因素。企业和博客主经常面临挑战&#xff0c;如何制作高质量、无误的内容以吸引更多的在线用户。此文将详细分析“爱校对”如何帮助用户优化内容&#xff0c;从而提高在线可见性。 1.互联网内容的挑战 搜索引擎…...

MATLAB中xlsread函数用法

目录 语法 说明 示例 将工作表读取到数值矩阵 读取元胞的范围 读取列 请求数值、文本和原始数据 对工作表执行函数 请求自定义输出 局限性 xlsread函数的功能是读取Microsoft Excel 电子表格文件 语法 num xlsread(filename) num xlsread(filename,sheet) num x…...

Prisma.js:JavaScript中的基于代码的ORM

​Prisma是一种流行的用于服务器端JavaScript和TypeScript的数据映射层&#xff08;ORM&#xff09;。它的核心目的是简化和自动化数据在存储和应用程序代码之间的传输方式。Prisma支持各种数据存储&#xff0c;并为数据持久化提供了一个强大而灵活的抽象层。通过这个基于代码的…...

解决问题:在cocos create中如何从b文件调用到a文件里用CC.resource.load动态加载的图集

目录 1.在a文件中定义一个公共的变量存储动态加载的图集 2.在a.js中添加一个静态方法&#xff0c;返回动态加载的图集 3.在b.js中使用a.js中定义的静态方法获取图集&#xff0c;并使用它 假设a文件中用CC.resource.load动态加载了一张图集&#xff0c;b文件需要使用这张图集&am…...

分布式 - 消息队列Kafka:Kafka 消费者消费位移的提交方式

文章目录 1. 自动提交消费位移2. 自动提交消费位移存在的问题&#xff1f;3. 手动提交消费位移1. 同步提交消费位移2. 异步提交消费位移3. 同步和异步组合提交消费位移4. 提交特定的消费位移5. 按分区提交消费位移 4. 消费者查找不到消费位移时怎么办&#xff1f;5. 如何从特定…...

如何利用 ChatGPT 进行自动数据清理和预处理

推荐&#xff1a;使用 NSDT场景编辑器助你快速搭建可二次编辑的3D应用场景 ChatGPT 已经成为一把可用于多种应用的瑞士军刀&#xff0c;并且有大量的空间将 ChatGPT 集成到数据科学工作流程中。 如果您曾经在真实数据集上训练过机器学习模型&#xff0c;您就会知道数据清理和预…...

PHP“牵手”淘宝商品评论数据采集方法,淘宝API接口申请指南

淘宝天猫商品评论数据接口 API 是开放平台提供的一种 API 接口&#xff0c;它可以帮助开发者获取商品的详细信息&#xff0c;包括商品的标题、描述、图片等信息。在电商平台的开发中&#xff0c;详情接口API是非常常用的 API&#xff0c;因此本文将详细介绍详情接口 API 的使用…...

你更喜欢哪一个:VueJS 还是 ReactJS?

观点列表&#xff1a; 1、如果你想在 HTML 中使用 JS&#xff0c;请使用 Vue&#xff1b; 如果你想在 JS 中使用 HTML&#xff0c;请使用 React。 当然&#xff0c;如果您希望在 JS 中使用 HTML&#xff0c;请将 Vue 与 JSX 结合使用。 2、Svelte&#xff1a;我喜欢它&#…...

PyTorch学习笔记(十六)——利用GPU训练

一、方式一 网络模型、损失函数、数据&#xff08;包括输入、标注&#xff09; 找到以上三种变量&#xff0c;调用它们的.cuda()&#xff0c;再返回即可 if torch.cuda.is_available():mynn mynn.cuda() if torch.cuda.is_available():loss_function loss_function.cuda(…...

【实战】十一、看板页面及任务组页面开发(三) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十五)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理七、Hook&…...

stm32G473的flash模式是单bank还是双bank?

今天突然有人stm32G473的flash模式是单bank还是双bank&#xff1f;由于时间太久&#xff0c;我真忘记了。搜搜发现&#xff0c;还真有人和我一样。见下面的链接&#xff1a;https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

大话软工笔记—需求分析概述

需求分析&#xff0c;就是要对需求调研收集到的资料信息逐个地进行拆分、研究&#xff0c;从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要&#xff0c;后续设计的依据主要来自于需求分析的成果&#xff0c;包括: 项目的目的…...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

【Linux】C语言执行shell指令

在C语言中执行Shell指令 在C语言中&#xff0c;有几种方法可以执行Shell指令&#xff1a; 1. 使用system()函数 这是最简单的方法&#xff0c;包含在stdlib.h头文件中&#xff1a; #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)

概述 在 Swift 开发语言中&#xff0c;各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过&#xff0c;在涉及到多个子类派生于基类进行多态模拟的场景下&#xff0c;…...

关于nvm与node.js

1 安装nvm 安装过程中手动修改 nvm的安装路径&#xff0c; 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解&#xff0c;但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后&#xff0c;通常在该文件中会出现以下配置&…...

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

Go 并发编程基础:通道(Channel)的使用

在 Go 中&#xff0c;Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式&#xff0c;用于在多个 Goroutine 之间传递数据&#xff0c;从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...