Redis从入门到精通(四)Redis实战:短信登录

文章目录

  • 前言
  • 第4章 Redis实战:短信登录
    • 4.1 基于session实现短信登录
      • 4.1.1 短信登录逻辑梳理
      • 4.1.2 创建测试项目
      • 4.1.3 实现发送短信验证码功能
      • 4.1.4 实现用户登录功能
      • 4.1.5 实现登录拦截功能
      • 4.1.6 session共享问题
    • 4.2 基于Redis实现短信登录
      • 4.2.1 Key-Value的结构设计
      • 4.2.2 发送短信验证码功能改造
      • 4.2.3 用户登录功能改造
      • 4.2.4 登录拦截功能改造

前言

前面三章我们对Redis的基础知识进行了深入的学习,已经掌握了Redis的基本使用方法。

Redis从入门到精通(一)Redis安装与启动、Redis客户端的使用
Redis从入门到精通(二)Redis的数据类型和常见命令介绍
Redis从入门到精通(三)Jedis客户端、SpringDataRedis客户端

接下来的第4章开始进入实战环节,来学习一个Redis实战项目:短信登录。

第4章 Redis实战:短信登录

短信登录功能可以基于session实现,也可以基于Redis实现,下面分别介绍这两种方式。

4.1 基于session实现短信登录

4.1.1 短信登录逻辑梳理

  • 1)发送验证码

    用户在登录页面输入手机号,点击“发送验证码”按钮。后台收到请求后,校验手机号是否符合格式,如果不符合,则要求用户重新输入手机号。

    如果符合,后台随机生成6位数字的验证码,并将验证码保存到session,然后再通过短信的方式将验证码发送给用户(由于没有短信网关,可以使用打印日志的方式模拟)。

  • 2)登录与注册

    用户获取到验证码后,输入手机号和验证码,并点击“登录”按钮。后台收到请求后,从session中获取之前保存好的验证码,并与用户提交的验证码进行比对,如果不一致,则登录失败。

    如果一致,则根据手机号查询数据库的用户信息,如果用户不存在,则创建一个新的用户保存到数据库,如果存在,则直接获取;然后将用户信息保存到session中,方便后续获取当前登录用户信息。

  • 3)校验登录状态

    用户发起的请求,除了获取验证码、用户登录等少数指定的请求外,一般都要求用户必须处于登录状态。如果用户并非处于登录状态,说明这是一个非法请求,必须进行拦截。

    用户发起这些请求时,后台进行拦截,然后从session中拿到用户信息。如果没有获取到用户信息,则表示没有用户没有登录,要进行拦截。如果获取到了用户信息,则说明用户已经登录了,则放行。

在这里插入图片描述

4.1.2 创建测试项目

下面以一个SpringBoot项目来进行测试。

由于项目的创建不是学习的重点,这里不进行详述。该测试项目的代码已打包上传,有需要请到本文顶部下载绑定的代码资源。

4.1.3 实现发送短信验证码功能

  • 接口文档

    项目说明
    请求方式GET
    请求路径/user/code
    请求参数phone
    返回值
  • 代码实现

controller目录下的UserController类中实现该接口:

@GetMapping("/code")
public BaseResult<String> sendCode(String phone, HttpSession httpSession) {// 1.校验手机号格式if(RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return BaseResult.setFail("手机号输入有误!");}// 3.符合,随机生成6位数验证码String code = String.valueOf((int)(Math.random() * 900000 + 100000));// 4.将验证码保存到sessionhttpSession.setAttribute("code", code);// 5.短信方式发送验证码log.info("发送短信验证码成功,验证码:{}", code);// 6.返回成功return BaseResult.setOk("短信验证码已成功发送至手机号" + phone + ",请注意查收!");
}
  • 功能测试

在这里插入图片描述

在这里插入图片描述

特别要注意的是,由于我们是使用HTTP工具进行发包测试的,所以需要设置一下Cookies,因为后端是利用Cookies中的JSESSIONID参数来创建session的。为了确保多次请求拿到的session是同一个,Cookies也必须要一致。

在这里插入图片描述

4.1.4 实现用户登录功能

  • 接口文档

    项目说明
    请求方式POST
    请求路径/user/login
    请求参数phone,code
    返回值
  • 代码实现

在UserController类中编写一个用户登录方法:

@Resource
private IUserService userService;@PostMapping("/login")
public BaseResult login(@RequestBody LoginForm loginForm, HttpSession httpSession) {log.info("用户开始登录...{}", loginForm.toString());// 1.校验手机号if(RegexUtils.isPhoneInvalid(loginForm.getPhone())) {// 2.如果不符合,返回错误信息return BaseResult.setFail("手机号输入有误!");}// 3.从session中获取验证码并校验Object cacheCode = httpSession.getAttribute("code");if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())) {// 4.验证码不一致,返回错误信息return BaseResult.setFail("验证码错误!");}// 5.一致,根据手机号查询用户User user = userService.query().eq("phone", loginForm.getPhone()).one();if(user == null) {// 6.用户不存在,则创建一个用户user = new User();user.setPhone(loginForm.getPhone());user.setNickName(loginForm.getPhone());userService.save(user);}// 7.将用户信息保存到session中httpSession.setAttribute("user", user);log.info("{} 登录成功...", loginForm.getPhone());return BaseResult.setOk("登录成功");
}
  • 功能测试

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.1.5 实现登录拦截功能

  • 接口文档

    用户发起的请求,除了获取验证码、登录等少数指定的请求外,一般都要求用户必须处于登录状态。如果用户并非处于登录状态,说明这是一个非法请求,必须进行拦截。

    可以通过拦截器来实现这个功能。

  • 代码实现

    要创建一个拦截器,只需要创建一个类LoginInterceptor,实现org.springframework.web.servlet.HandlerInterceptor接口,并重写其preHandle()方法。

    public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取sessionHttpSession session = request.getSession();// 2.获取session中的用户Object user = session.getAttribute("user");// 3.判断用户是否存在if(user == null){// 4.不存在,拦截,返回401状态码response.setStatus(401);return false;}// 5.存在,放行return true;}
    }
    

    其次,要对自定义的拦截器进行注册,让其生效:

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器,排除获取验证码和登录请求registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login").order(1);}
    }
    
  • 功能测试

在未登录的情况下发送请求/user/info,报401,说明没有通过拦截器的校验:

在这里插入图片描述

4.1.6 session共享问题

基于session实现短信登录,在服务端单机的情况下是没问题的,但如果服务端采用集群方式,则会出现session共享问题。

每个tomcat中都有一份属于自己的session。假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上肯定没有第一台服务器存放的session,所以此时整个登录拦截功能就会出现问题。

早期的解决方案是session拷贝,即当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样就可以实现session的共享。但这种方法也有弊端:第一,每台服务器中都有完整的一份session数据,服务器压力过大;第二,session拷贝数据时,可能会出现延迟

基于此,更好的解决方案是基于Redis来完成,而且Redis数据本身就是共享的。

4.2 基于Redis实现短信登录

4.2.1 Key-Value的结构设计

由于本案例中要存入Redis的数据比较简单,因此可以考虑使用String类型或Hash类型来存储数据。

这两种方式各有优点,String类型以JSON字符串保存数据,比较直观;而Hash类型可以将对象的每个字段独立存储,可以针对单个字段做CRUD,比较方便。最终根据实际需要选择即可,本案例选择使用String类型。

在基于session实现时,每个用户都有一个独享的session。但Redis的Key是共享的,因此不能再使用基于session方式中的"code""user"作为Key值。

在设计Key时,需要满足两点要求:第一,Key要有唯一性;第二,Key要方便携带。

在本案例中,如果采用手机号作为Key当然可以,它具备唯一性且方便携带,并且和验证码息息相关。但从安全角度看,手机号毕竟属于敏感数据,每次请求都携带手机号是不合适的。

综合考虑,本案例将采用login:code:{phone}作为保存验证码的Key;而保存用户信息的Key,会在后台生成一个随机串token,采用login:user:{token}作为Key,让用户每次请求都携带这个token。

4.2.2 发送短信验证码功能改造

  • 代码实现(关注修改部分)
@Resource
private StringRedisTemplate stringRedisTemplate;@GetMapping("/code")
public BaseResult<String> sendCode(String phone, HttpSession httpSession) {// 1.校验手机号if(RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return BaseResult.setFail("手机号输入有误!");}// 3.符合,随机生成验证码String code = String.valueOf((int)(Math.random() * 900000 + 100000));// 4.将验证码保存到session// httpSession.setAttribute("code", code);// 修改:将验证码保存到Redis// 采用 login:code:{phone} 作为保存验证码的KeystringRedisTemplate.opsForValue().set("login:code:" + phone, code);// 5.短信方式发送验证码log.info("发送短信验证码成功,验证码:{}", code);// 6.返回成功return BaseResult.setOk("短信验证码已成功发送至手机号" + phone + ",请注意查收!");
}
  • 功能测试

调用获取验证码接口/user/code?phone=18922102123后,查看Redis中的数据:

在这里插入图片描述

4.2.3 用户登录功能改造

  • 代码实现(关注修改部分)
@PostMapping("/login")
public BaseResult<String> login(@RequestBody LoginForm loginForm, HttpSession httpSession) throws JsonProcessingException {log.info("用户开始登录...{}", loginForm.toString());// 1.校验手机号if(RegexUtils.isPhoneInvalid(loginForm.getPhone())) {// 2.如果不符合,返回错误信息return BaseResult.setFail("手机号输入有误!");}// 3.从session中获取验证码并校验// Object cacheCode = httpSession.getAttribute("code");// 修改:从Redis中获取验证码String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + loginForm.getPhone());if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())) {// 4.验证码不一致,返回错误信息return BaseResult.setFail("验证码错误!");}// 5.一致,根据手机号查询用户User user = userService.query().eq("phone", loginForm.getPhone()).one();if(user == null) {// 6.用户不存在,则创建一个用户user = new User();user.setPhone(loginForm.getPhone());user.setNickName(loginForm.getPhone());userService.save(user);}// 7.将用户信息保存到session中// httpSession.setAttribute("user", user);// 修改:将用户信息保存到Redis中// 随机生成tokenString token = UUID.randomUUID().toString();log.info("token = {}", token);// 保存到RedisstringRedisTemplate.opsForValue().set("login:user:" + token, new ObjectMapper().writeValueAsString(user));// 设置token有效期:2小时stringRedisTemplate.expire("login:user:" + token, 2, TimeUnit.HOURS);log.info("{} 登录成功...", loginForm.getPhone());// 将token返回给前端return BaseResult.setOkWithData(token);
}
  • 功能测试

调用用户登录接口/user/login后,查看Redis中的数据:

在这里插入图片描述

在这里插入图片描述

4.2.4 登录拦截功能改造

  • 代码实现(关注修改部分)
@Slf4j
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.获取session// HttpSession session = request.getSession();// 2.获取session中的用户// Object user = session.getAttribute("user");// 修改:基于用户token获取Redis中的用户信息// 获取用户携带的tokenString token = request.getHeader("authorization");log.info("token from client => {}", token);// 基于token获取Redis中的用户信息String userJosn = stringRedisTemplate.opsForValue().get("login:user:" + token);log.info("user from redis => {}", userJosn);// 转为Java对象User user = null;if(StrUtil.isNotBlank(userJosn)) {user = new ObjectMapper().readValue(userJosn, User.class);}// 3.判断用户是否存在if(user == null){// 4.不存在,拦截,返回401状态码response.setStatus(401);return false;}// 5.存在,放行return true;}
}

注册LoginInterceptor时传入StringRedisTemplate实例:

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器,排除获取验证码和登录请求registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/user/code","/user/login").order(1);}
}
  • 功能测试

调用查询用户详情接口/user/info(项目中暂未编写该Controller方法)。当携带一个错误token时,报401,说明未通过拦截器校验:

在这里插入图片描述

携带一个正确token,报404,说明已经通过了拦截器校验:

在这里插入图片描述

本节完,更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • 再探Java为面试赋能(持续更新中…)

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

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

相关文章

mysql语句学习

SQL Select语句完整的执行顺序&#xff1a; 1、from子句组装来自不同数据源的数据&#xff1b; &#xff08;先join在on&#xff09; 2、where子句基于指定的条件对记录行进行筛选&#xff1b; 3、group by子句将数据划分为多个分组&#xff1b; 4、使用聚集函数进行计算&a…

取证之内存取证工具Volatility学习

一、简介 Volatility是一款开源的内存取证分析工具&#xff0c;支持Windows&#xff0c;Linux&#xff0c;MaC&#xff0c;Android等多类型操作系统系统的内存取证方式。该工具是由python开发的&#xff0c;目前支持python2、python3环境。 二、安装 1、下载地址 GitHub - …

搭建 Qt 开发环境

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;QT❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、QT SDK 的下载和安装 1.QT SDK 的下载 二、QT SDK的安装 1、找到下载的文件并双击 2、双击之…

Gatekeep AI:文本转视频教学工具,开启智能学习新纪元

在当今的数字时代,技术的进步不断改变着我们学习和理解知识的方式。 Gatekeep AI 就是这样一款令人兴奋的工具,它专注于将数学和物理问题通过文本提示转化为生动的视频。 特点与优势: 直观的可视化:将复杂的数学和物理概念以直观的视频形式呈现。快速生成:根据用户提供的…

IT公司管理者日常工作思考

一、前言 作为IT公司的管理者,我们应该一切从实际出发,理论和实际相结合,以终为始,带领公司(组织)不断前进。当然前进包括稳重求进,稳步前进,积极扩张,厚积薄发。等等。大多数公司追求的都是稳中求进,没有稳的进都是在冒比较大的风险。积极扩张,又容易出现较大的风…

世优科技上榜2024年度《中国虚拟数字人影响力指数报告》

日前&#xff0c;第三期《中国虚拟数字人影响力指数报告》在中国网络视听大会上正式发布。本期《报告》由中国传媒大学媒体融合与传播国家重点实验室&#xff08;以下简称“国重实验室”&#xff09;、中国传媒大学数字人研究院编制&#xff0c;中国网络视听协会、人民日报智慧…

数据库加载驱动问题(java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver)

java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver 遇到此问题&#xff0c;首先检查IDEA外部库中是否有mysql数据库驱动。如下所示&#xff1a; 如果发现外部库中存有mysql数据库驱动&#xff0c;需要在数据库配置文件中查看是否设置有时区mysql8.0以上版本需要设…

SAD法(附python实现)和Siamese神经网络计算图像的视差图

1 视差图 视差图&#xff1a;以左视图视差图为例&#xff0c;在像素位置p的视差值等于该像素在右图上的匹配点的列坐标减去其在左图上的列坐标 视差图和深度图&#xff1a; z f b d z \frac{fb}{d} zdfb​ 其中 d d d 是视差&#xff0c; f f f 是焦距&#xff0c; b b…

【漏洞复现】用友NC-Cloud系统queryRuleByDeptId存在SQL注入漏洞

“ 如棠安全的技术文章仅供参考&#xff0c;此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等&#xff08;包括但不限于&#xff09;进行检测或维护参考&#xff0c;未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的…

vue 实现的h5 页面,如何设置页面中的 title

修改页面中的title 公共修改方式在App.vue 中&#xff1a; created() {document.title "测试标题"; },单个页面修改&#xff0c;就在单个页面编写就ok

提高 API 性能的小技巧

引言 随着数字时代的到来&#xff0c;API&#xff08;应用程序接口&#xff09;已经成为连接不同服务和应用的桥梁&#xff0c;其意义远超技术工具本身。随着大数据、云服务和 5G 技术等领域的进步&#xff0c;API 的作用变得更加重要&#xff0c;它不仅促进了数字转型的发展&…

supersqli-攻防世界

题目 加个报错 1 and 11 #没报错判断为单引号字符注入 爆显位 1 order by 2#回显正常 1 order by 3#报错 说明列数是2 尝试联合查询 -1 union select 1,2# 被过滤了 return preg_match("/select|update|delete|drop|insert|where|\./i",$inject); select|update|d…

时间管理系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)大学生

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读300套最新项目持续更新中..... 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含ja…

git上传到本都仓库

摘要&#xff1a;本地初始化init仓库&#xff0c;进行pull和push&#xff1b;好处是便于利用存储设备进行git备份 git init --bare test.git 随便到一个空的目录下git clone 然后使用git上传 把git仓库删除之后再clone一次验证一下是否上传成功&#xff1a; 如果在ubantu上面没…

数据资产盘点七步法:教你为什么盘,盘什么,怎么盘

数据作为企业一种“特殊资产”&#xff0c;已被列入企业的资产负债表。只有对数据资源进行统筹规划&#xff0c;全面梳理&#xff0c;“摸清家底”&#xff0c;才能让数据更好地服务于企业的业务应用。怎样识别数据资产、有效管理和运营数据资产&#xff0c;利用现有的数据资产…

05 | Swoole 源码分析之 WebSocket 模块

首发原文链接&#xff1a;Swoole 源码分析之 WebSocket 模块 大家好&#xff0c;我是码农先森。 引言 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许客户端和服务器之间进行实时数据传输。 与传统的 HTTP 请求-响应模型不同&#xff0c;WebSocket 可以保持…

node res.end返回json格式数据

使用 Node.js 内置 http 模块的createServer()方法创建一个新的HTTP服务器并返回json数据&#xff0c;代码如下&#xff1a; const http require(http);const hostname 127.0.0.1; const port 3000;const data [{ name: 测试1号, index: 0 },{ name: 测试2号, index: 1 },…

[计算机效率] 格式转换工具:格式工厂

3.14 格式转换工具&#xff1a;格式工厂 格式工厂是一款功能强大的多媒体格式转换软件&#xff0c;可以实现音频、视频、图片等多种格式的转换。它支持几乎所有类型的多媒体格式&#xff0c;包括视频、音频、图片、字幕等&#xff0c;可以轻松实现格式之间的转换&#xff0c;并…

手写红黑树【数据结构】

手写红黑树【数据结构】 前言版权推荐手写红黑树一、理论知识红黑树的特征增加删除 二、手写代码初始-树结点初始-红黑树初始-遍历初始-判断红黑树是否有效查找增加-1.父为黑&#xff0c;直接插入增加-2. 父叔为红&#xff0c;颜色调换增加-3. 父红叔黑&#xff0c;颜色调换&am…

C++ Primer 总结索引 | 第十二章:动态内存

1、到目前为止&#xff0c;我们编写的程序中 所使用的对象 都有着严格定义的生存期。全局对象 在程序启动时分配&#xff0c;在程序结束时 销毁。对于 局部自动对象&#xff0c;当我们进入 其定义所在的程序块时被创建&#xff0c;在 离开块时销毁。局部static对象 在第一次使用…