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

文章目录

  • 前言
  • 第4章 Redis实战
    • 4.1 短信登录
      • 4.1.1 基于session实现短信登录
        • 4.1.1.1 短信登录逻辑梳理
        • 4.1.1.2 创建测试项目
        • 4.1.1.3 实现发送短信验证码功能
        • 4.1.1.4 实现用户登录功能
        • 4.1.1.5 实现登录拦截功能
        • 4.1.1.6 session共享问题
      • 4.1.2 基于Redis实现短信登录
        • 4.1.2.1 Key-Value的结构设计
        • 4.1.2.2 发送短信验证码功能改造
        • 4.1.2.3 用户登录功能改造
        • 4.1.2.4 登录拦截功能改造

前言

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

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

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

第4章 Redis实战

4.1 短信登录

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

4.1.1 基于session实现短信登录

4.1.1.1 短信登录逻辑梳理
  • 1)发送验证码

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

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

  • 2)登录与注册

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

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

  • 3)校验登录状态

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

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

在这里插入图片描述

4.1.1.2 创建测试项目

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

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

4.1.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.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.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.1.6 session共享问题

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

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

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

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

4.1.2 基于Redis实现短信登录

4.1.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.1.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.1.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.1.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/795979.shtml

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

相关文章

顺子日期(结果填空)

为了解决这个问题&#xff0c;我们需要遍历2022年的每一天&#xff0c;并检查日期的每一位数字以查找顺子。下面是一个Java程序&#xff0c;用于计算2022年中的顺子日期数量&#xff1a; public class Main {public static void main(String[] args) {int count 0; for (int…

[StartingPoint][Tier0]Preignition

Task 1 Directory Brute-forcing is a technique used to check a lot of paths on a web server to find hidden pages. Which is another name for this? (i) Local File Inclusion, (ii) dir busting, (iii) hash cracking. (目录暴力破解是一种用于检查 Web 服务器上的大…

leetcode代码记录(有效的字母异位词

目录 1. 题目&#xff1a;2. 我的代码&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断它们是不是一组变位词&#xff08;字母异位词&#xff09;。 注意&#xff1a;若 s 和 t 中每个字符出现的次数都相同且字符顺序…

【简单讲解下epoll】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

循环神经网络:揭秘长期记忆的魔法之源

在人工智能和机器学习领域&#xff0c;循环神经网络&#xff08;Recurrent Neural Networks&#xff0c;简称RNN&#xff09;以其独特的架构和机制&#xff0c;在处理序列数据方面展现出了强大的能力。特别值得一提的是&#xff0c;RNN能够学习到长期的记忆&#xff0c;这使得它…

ubuntu-server部署hive-part1-安装jdk

参照 https://blog.csdn.net/qq_41946216/article/details/134345137 操作系统版本&#xff1a;ubuntu-server-22.04.3 虚拟机&#xff1a;virtualbox7.0 安装jdk 上传解压 以root用户&#xff0c;将jdk上传至/opt目录下 tar zxvf jdk-8u271-linux-x64.tar.gz 配置环境变量…

网址打包微信小程序源码 wap转微信小程序 网站转小程序源码 网址转小程序开发

内容目录 一、详细介绍二、效果展示2.效果图展示 三、学习资料下载 一、详细介绍 我们都知道微信小程序是无法直接打开网址的。 这个小程序源码提供了一种将网址直接打包成微信小程序的方法&#xff0c; 使得用户可以在微信小程序中直接访问这些网址内容。 这个源码没有进行加…

律所如何做好内容运营,提升品牌影响力

近年来&#xff0c;随着品牌推广方式的改变&#xff0c;中国律所也开始关注内容营销&#xff0c;期待能够凭借内容营销增强影响力。今天&#xff0c;媒介盒子就从内容传播的逻辑出发&#xff0c;和大家聊聊律所如何做好内容运营&#xff0c;提升品牌影响力。 一、品牌形象管理 …

蜂窝物联:智慧禽畜养殖解决方案

蜂窝物联&#xff1a;智慧禽畜养殖解决方案是基于物联网技术&#xff0c;在线监测动物生长的环境信息&#xff0c;通过氨气传感器、二氧化碳传感器、湿度传感器等设备监测舍内的各项环境参数&#xff0c;自动把畜舍内的实时环境参数上传至监控软件平台&#xff0c;并联动控制风…

C++11特性详解(万字)

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 统一的初始化列表 {}初始化 这种初始化方式我们建议用第一种&#xff0c;但是以后看见下面两种也不要感到疑惑&#xff0c;是可以这样初始化的。 内置类型初始化 int main() {int a 1;int b { 1 };int c{ 1 };return 0…

kubectl explain资源文档命令

学习并使用了一段时间的kubernetes&#xff0c;发现对k8s还是了解甚少&#xff0c;于是利用上下班通勤的时间又去B站看一些大佬的视频&#xff0c;又来重学巩固一遍知识&#xff0c;并做些记录。 之前在学习使用过程中未成了解过explain这个命令&#xff0c;因为自己部署的版本…

Java笔试题总结

HashSet子类依靠()方法区分重复元素。 A toString(),equals() B clone(),equals() C hashCode(),equals() D getClass(),clone() 答案:C 解析: 先调用对象的hashcode方法将对象映射为数组下标,再通过equals来判断元素内容是否相同 以下程序执行的结果是&#xff1a; class X{…

Linux:权限篇

文章目录 前言1.用户2.文件的权限管理2.1 修改文件的权限2.2 修改文件的拥有者2.3 修改文件的所属组 3.file指令4.umask指令4.目录的权限管理总结 前言 Linux权限在两个地方有所体现&#xff0c;一种是使用用户&#xff1a;分为root超级用户员与普通用户。另一个是体现在文件的…

【数据结构与算法】力扣 24. 两两交换链表中的节点

题目描述 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a; head [1,2,3,4] 输出&#…

IDEA2023创建SpringMVC项目

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 开发环境篇 ✨特色专栏&#xff1a; M…

Linux使用docker集群部署

目录 一&#xff0c;环境准备 1.1.安装docker 1.2.配置镜像就加速器 二&#xff0c;docker部署 2.1.创建网络 ​编辑 2.2.安装redis 2.3.安装MySQL 2.4.安装JDK,TomCat及部署后台项目 2.5.安装Nginx及配置静态资源 一&#xff0c;环境准备 1.1.安装docker 安装yum-u…

Spring Boot-01-通过一个项目快速入门

官方参考文档&#xff1a;Spring Boot Reference Documentation 0. 概述 Spring的缺点&#xff1a; 1. 配置繁琐&#xff1a;虽然Spring的组件代码是轻量级&#xff0c;但它的配置却是重量级的。 2. 依赖繁琐&#xff1a;项目的依赖管理也是一件耗时耗力的事情。分析要导入哪…

动手学深度学习 | Jupyter Notebook 打开指定目录

目录 1 下载课件代码 2 在目录中打开 3 展示目录失败 3.1 问题分析 3.2 问题解决 ⚠️ 写在前面&#xff1a; 无语子&#xff0c;打开个目录花了我一天时间下文中提到的 “d2l-zh” 和 “pytorch” 均为目录名jupyter notebook 的安装请参考其它博客 1 下载课件代码…

计算机出现msvcr110.dll丢失是什么意思?七种方法解决msvcr110.dll丢失

msvcr110.dll文件是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;由Microsoft Corporation开发。它是Visual C Redistributable for Visual Studio 2012的必要部分&#xff0c;包含了C运行时库函数的代码&#xff0c;这些函数为执行C/C应用程序提供了基础服务。这…

基于Socket简单的TCP网络程序

⭐小白苦学IT的博客主页 ⭐初学者必看&#xff1a;Linux操作系统入门 ⭐代码仓库&#xff1a;Linux代码仓库 ❤关注我一起讨论和学习Linux系统 TCP单例模式的多线程版本的英汉互译服务器 我们先来认识一下与udp服务器实现的不同的接口&#xff1a; TCP服务器端 socket()&…