Redis:原理+项目实战——Redis实战2(Redis实现短信登录(原理剖析+代码优化))

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理+项目实战——Redis实战1(session实现短信登录(并剖析问题))
📚订阅专栏:Redis速成
希望文章对你们有所帮助

Redis实现短信登录

  • 基于Redis实现共享session项目
    • Redis替代session的业务流程
      • 发送短信验证码
      • 短信验证码登录与注册
      • 校验登录状态
      • 关键点实现
    • 基于Redis实现短信登录
      • 发送验证码
      • 登录验证功能
    • 解决状态登录刷新的问题——登录拦截器的优化

基于Redis实现共享session项目

Redis替代session的业务流程

发送短信验证码

这个大致的流程是跟session的业务流程差不多的,无非就是验证码不再保存到session中,而是保存到了Redis中,Redis的结构是key-value的,且value是很多种类型的,在这里我们选择最简单的String类型即可。

一个需要考虑的问题是key的选取,在session中我们选用了“code”来作为key,但在这里却不行。这是因为每一个不同的浏览器在发送请求的时候都会有一个不同的独立的session,也就是说Tomcat的内部维护了很多的session,互相之间是不会干扰的。但是Redis是一个共享的内存空间,如果直接使用key是肯定会造成覆盖这种不好的局面的,所以我们不能直接选用“code”来作为key。

容易发现,每个手机号都不一样,因此我们可以直接用手机号作为key。

短信验证码登录与注册

最终的用户信息不再保存到session中,而是保存都Redis中去了,同样要考虑key跟value的选择:
(1)value的选取:
我们要保存的是用户的信息,这是一个对象。Redis中的String可以将用户信息以JSON字符串的形式来保存,Hash可以将对象中的每个字段独立存储。具体的大家可以看我之前的文章:
Redis:原理速成+项目实战——Redis常见命令(数据结构、常见命令总结)
明显我们用Hash结构是最合适的。
(2)key的选取:
这里并不建议用phone作为key,而是以随机token(服务器生成的令牌)为key来存储用户数据,具体原因会在后面进行解释。

在之前我们校验登录状态的时候,是从cookie中获取session再得到用户信息,而现在我们校验登录的时候要访问的凭证就是这个随机token了,但Tomcat不会将这个token自动写到浏览器上面。
所以我们把数据保存到Redis以后还需要手动的把token返回到前端,流程就得修改:

1、提交手机号和验证码
2、校验验证码
3、根据手机号查询用户信息
4、用户保存到Redis
5、返回token给客户端(重要一步)

校验登录状态

我们不再是从浏览器中的cookie指定的session来获取用户信息,而是以随机token为key来从Redis中获取信息,流程如下:

1、用户发送请求并携带token
2、从Redis中获取用户(以随机token为key)
3、判断用户是否存在:
(1)没有这个用户就拦截
(2)有这个用户就保存用户信息到ThreadLocal,并放行

关键点实现

我们在校验登录状态的时候,需要携带token,这是如何做到的呢?这就涉及到了前端的知识了:
在这里插入图片描述
在login方法的axios请求的相应里,我们将登录凭证直接放到了session中,而我们之后的每次请求都要携带这个token,我们可以在axios里面进行实现:
在这里插入图片描述
所以,我们的key肯定不能再选择手机号了,因为这种存储到前端代码的行为并不是安全的。

基于Redis实现短信登录

发送验证码

直接将上一篇文章的代码进行修改:

	//通过Resource注解注入SpringData提供的API@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}String code = RandomUtil.randomNumbers(6);/*** session.setAttribute("code", code);* 保存验证码到session,这个过程被替代成保存到Redis!*//*** 保存验证码到Redis,其中key是phone(加一下业务前缀防止冲突),value是String类型* 我们要对key设置一下有效期为2分钟,防止网站被无限的注册攻击而导致内存爆炸* 代码中的前缀、有效时间用常量来替代,常量另外保存到其他的类中,看起来更规范*///stringRedisTemplate.opsForValue().set("login:code" + phone, code, 2, TimeUnit.MINUTES);stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);log.debug("成功发送短信验证码:{}", code);return Result.ok();}

登录验证功能

根据流程更新login方法:

	@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {//不符合,返回错误信息return Result.fail("手机号格式错误");}// TODO 从Redis中获取验证码来做校验String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)){//不一致,报错return Result.fail("验证码错误");}//一致,根据手机号查询用户,这里要单表查询//mybatis-plus可以帮助我们很快的实现://1、继承类ServiceImpl<实体类的Mapper,实体类>//2、实体类中要加入注解@TableName(),表示从哪个数据库取的//3、调用query()方法可以直接实现select * from 表//4、调用eq方法验证查询出来的数据中,列名为phone的列有没有值与phone相同的//5、可以通过one()查询出一个用户,也可以list()查询出多个用户,这里显然只会有一个User user = query().eq("phone", phone).one();//判断用户是否存在if (user == null){//不存在,创建新用户并保存user = createUserWithPhone(phone);}/*** session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));* 之前保存用户信息到session中,现在改成保存到Redis中去*///随机生成token,作为登录令牌String token = UUID.randomUUID().toString(true);/***  将User对象转换为Hash存储*  1、转换成UserDTO*  2、将其转换成Map的形式*  3、用Hash结构的putAll方法,因为UserDTO还是包含了多个字段和字段值*  4、要给token设置一个有效期,30min没操作就退出登录(效仿session),putAll没有对应的参数选择,要单独用expire()设*  5、要注意一个细节,每次用户操作了就要重新去更新这30min(这里我们可以用拦截器来看用户什么时候操作了系统)*/UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);String tokenKey = LOGIN_USER_KEY + token;//LOGIN_USER_KEY="login:login"//存储stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);//设置有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);//LOGIN_USER_TTL=30L//返回tokenreturn Result.ok(token);}

从之前的session改为现在的token,我们拦截器当然也要进行修改,将放行的一些条件进行修改:

	private StringRedisTemplate stringRedisTemplate;//这里只能自己写构造函数来注入,没办法用@autowired注解,因为LoginInterceptor的对象是手动new出来的public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {//是否为空response.setStatus(401);return false;}//基于token获取Redis中的用户String key = RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//判断用户是否存在if (userMap.isEmpty()) {//不存在,拦截,并返回401错误码response.setStatus(401);return false;}//将查询到的Hash数据转回DTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//存在,保存用户信息到ThreadLocalUserHolder.saveUser((UserDTO) userDTO);//刷新token的有效期stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//放行return true;}

容易发现,上面的代码没有使用@AutoWired进行注入,这是因为我们的这个类并不是Spring托管的类。
我们的MvcConfig也要进行修改:
在这里插入图片描述
这里我们可以用@Resource注解来获取StringRedisTemplate对象,这是因为我们的类已经加上了@Configuration注解,这样类已经是被Spring给托管了,可以使用该注解。
运行后,我们打开Redis的数据库,确实是把验证码给成功保存下来了:
在这里插入图片描述
但是我们在登录的时候会报类型转换错误的异常,这个出错出现在

stringRedisTemplate.opsForHash().putAll(tokenKey + token, userMap);

报错信息显示Long无法转换为String类型,说明我们的UserMap的类型出现了问题,UserMap来自于UserDTO,因此问题出现在了UserDTO这里:
在这里插入图片描述
这边的UserDTO中的id是Long类型的,而查看我们的StringRedisTemplate的源码:
在这里插入图片描述
它要求我们的key和value都是String类型的,因此我们需要修改代码,使得两者的类型要能够匹配:

方法一:不用BeanUtil.beanToMap方法,自行创建一个方法,手动将UserDTO里面的id先转换成String类型,然后存入Map。(这是我的方法,其实我觉得这个方法是最适合的,也容易想到)
方法二:继续使用BeanUtil.beanToMap这个方法,这个方法它允许我们对key和value做自定义。(这个方法是黑马程序员的讲解者提出的方法,我感觉跟炫技一样,这就是大佬)

这里就写一下第二个方法:

Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create() //CopyOptions表示做数据拷贝时候的选项.setIgnoreNullValue(true) //忽略空值.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));//转换为字符串

在这里插入图片描述
成功登录,同时我们也可以直接看到token的信息,登录验证也正是用这里的token来进行逻辑判断的。
我们可以总结一下Redis代替session需要考虑的三个问题:
1、数据结构的选取
2、key的选取
3、选择合适的存储粒度

解决状态登录刷新的问题——登录拦截器的优化

上述代码实现完还有一点小问题,之前的拦截器并不会拦截掉一切路径,而是所有需要登录的路径,那么会出现一个问题:我们的首页并不需要登录就可以直接访问,那么已经登录过的用户一直在首页进行操作,拦截器中的登录状态并不会刷新,就可能造成明明一直在操作系统,却被视为不算是在登录状态。
解决方法是再加上一个拦截器,用户的请求要先经过这个拦截器,这个拦截器会拦截一切的路径,所以我们可以在这个拦截器里面进行token有效期的刷新操作:

1、获取token
2、查询Redis的用户
3、保存到ThreadLocal
4、刷新token有效期
5、放行

这样的话,一切的请求都会触发刷新的操作。
那么之前的拦截器只需要查询ThreadLocal的用户,存在则继续,不存在则拦截。
我们可以在之前代码的基础上这样修改代码:
1、新增加一个拦截器,放行一切:

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;//这里只能自己写构造函数来注入,没办法用@autowired注解,因为LoginInterceptor的对象是手动new出来的public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {//为空也直接放行,判断交给下一个拦截器return true;}//基于token获取Redis中的用户String key = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//判断用户是否存在if (userMap.isEmpty()) {//不存在也放行return true;}//将查询到的Hash数据转回DTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);//刷新token的有效期stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户,防止内存泄漏UserHolder.removeUser();}
}

2、修改之前的拦截器,只需要进行用户的判断就可以了

package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断是否需要去拦截(ThreadLocal中是否有用户)if (UserHolder.getUser() == null){//没有,需要拦截,设置状态码response.setStatus(401);//拦截return false;}//有用户,则放行return true;}
}

3、重新配置拦截器:

package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;//添加拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {//登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","'user/me","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**").order(1);//order越大,执行优先级越小,表示更靠后的拦截器//token刷新的拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}

现在就实现了需求了,大家可以去不断的对系统进行操作,并且观察每个key的TTL。

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

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

相关文章

实现CodeWave 低代码开发平台快速应用开发的完整指南

目录 前言1 CodeWave开发流程2 应用创建2.1 新建应用2.2 从应用模板创建应用 3 数据模型设计3.1 实体设计3.2 结构设计3.3 枚举设计 4 逻辑设计4.1 查询数据源设置4.2 组件和属性配置4.3 属性设置与服务端全局变量 5 页面设计5.1 选择页面模板5.2 前端全局变量设计5.3 事件逻辑…

cocos creator + vscode debug

安装插件 安装插件&#xff1a;JavaScript Debugger 配置 7456 为本地cocos creator的启动端口 启动debug调试 选择对应的启动方式

彻底理解前端安全面试题(2)—— CSRF 攻击,跨站请求伪造攻击详解,建议收藏(含源码)

前言 前端关于网络安全看似高深莫测&#xff0c;其实来来回回就那么点东西&#xff0c;我总结一下就是 3 1 4&#xff0c;3个用字母描述的【分别是 XSS、CSRF、CORS】 一个中间人攻击。当然 CORS 同源策略是为了防止攻击的安全策略&#xff0c;其他的都是网络攻击。除了这…

免费下载的高清影像如何使用?

由于该影像数据为DAT格式&#xff0c;因此这里以“北京”影像数据为例&#xff0c;为大家分享一下它的查看与导出等使用方法。 卫星影像数据示例 我们上传到百度网盘中的北京高清卫星影像数据有37.8GB&#xff0c;除了卫星影像数据和地名标签外&#xff0c;还包括接图表和常用…

SpringBoot灵活集成多数据源(定制版)

如来说世界&#xff0c;非世界&#xff0c;是名世界 如来说目录&#xff0c;非目录&#xff0c;是名目录 前言前期准备代码实现演示扩展 前言 本篇博客基于SpringBoot整合MyBatis-plus&#xff0c;如果有不懂这个的&#xff0c; 可以查看我的这篇博客&#xff1a;快速CRUD的秘诀…

信息化和数字化的本质区别是什么?

信息化和数字化是两个概念的区别 它们有什么区别和联系呢&#xff1f;信息化&#xff1a;“业务数据化”&#xff0c;先让业务流程能被数据记录下来。信息化“业务数据化”。数字化&#xff1a;“数据业务化”&#xff0c;用已累积的业务数据去反哺优化业务流程。数字化“数据…

C++:继承(这一篇就够了)

C&#xff1a;继承&#xff08;这一篇就够了&#xff09; 一、继承的概念及定义1.1 继承的概念1.2 继承定义1.2.1定义格式1.2.2 继承关系和访问限定符1.2.3 继承基类成员访问方式的变化 二、基类和派生类对象赋值转换三、继承中的作用域四、派生类的默认成员函数五、继承与静态…

可穿戴智能设备应用领域以及使用意义分别有哪些?

可穿戴智能设备有哪些&#xff1f; 可穿戴智能设备是指可以佩戴在身上&#xff0c;具有智能功能和交互能力的电子设备。以下是一些常见的可穿戴智能设备&#xff1a; 智能手表&#xff1a;智能手表结合了传统手表的功能和智能设备的特性&#xff0c;可以显示时间、接收通知、监…

Linux下一切皆文件

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 你是否听过Linux下一切皆文件&#xff1f; 在哪里我们体悟到了Linux下一切皆文件&#xff1f; 文件是什么&#xff1f; 在哪里体悟到的&#xff1f; 常见疑惑 怎么办到的Linux下一切皆文件&#xff1f; 我们抛出…

LED显示屏常用驱动芯片一览表

LED显示屏驱动芯片是专门用于驱动LED显示屏的核心芯片&#xff0c;它能够将输入的电信号转化为驱动能力&#xff0c;以控制LED灯的亮度和颜色。LED显示屏驱动芯片具有高可靠性、低功耗、长寿命等优点&#xff0c;是LED显示屏产业的核心零部件之一。 SM16306SJ LED显示屏驱动芯…

《MySQL系列-InnoDB引擎02》InnoDB存储引擎介绍

文章目录 第二章 InnoDB存储引擎1 InnoDB存储引擎概述2 InnoDB存储引擎的版本3 InnoDB体系架构3.1 后台线程3.2 内存 4 Checkpoint技术5 Master Thread 工作方式5.1 InnoDB 1.0.x版本之前的Master Thread5.2 InnoDB 1.2.x版本之前的Master Thread5.3 InnoDB 1.2.x版本的Master …

python写入csv文件总是多出一行空行(windows)

问题代码 import csv from os import pathfull_path path.abspath(__file__) path_dir path.dirname(full_path)data [[Name, Age, City],[John, 25, New York],[Alice, 30, Los Angeles],[Bob, 35, Chicago] ] # 代码使用都是正常的&#xff0c;但是总是多出一行空格 with…

Unity游戏资源更新(AB包)

目录 前言&#xff1a; 一、什么是AssetBundle 二、AssetBudle的基本使用 1.AssetBundle打包 2.BuildAssetBundle BuildAssetBundleOptions BuildTarget 示例 3.AssetBundle的加载 LoadFromFile LoadFromMemory LoadFromMemoryAsync UnityWebRequestAsssetBundle 前…

nginx+rsyslog+kafka+clickhouse+grafana 实现nginx 网关监控

需求 我想做一个类似腾讯云网关日志最终以仪表方式呈现&#xff0c;比如说qps、p99、p95的请求响应时间等等 流程图 数据流转就像标题 nginx ----> rsyslog ----> kafka —> clickhouse —> grafana 部署 kafka kafka 相关部署这里不做赘述&#xff0c;只要创…

跨境电商代采是什么?怎么做代采网站?

跨境电商独立站就是跨境电商自行搭建的销售网站&#xff0c;服务器、域名都是自主购买的&#xff0c;并由跨境电商独立运营与营销推广。 近些年来&#xff0c;各类第三方电商平台虽然流量大&#xff0c;但是随着进驻电商数量的增加&#xff0c;流量竞争也愈发激烈&#xff0c;…

雷达遮挡检测综述

1 概述 雷达&#xff08;毫米波、激光&#xff09;的遮挡是实际项目中比较常见的现象&#xff0c;优秀的算法应当能够及时、准确地检测出雷达是否被遮挡&#xff0c;以及遮挡的严重程度&#xff0c;然后将故障信息发送给诊断系统&#xff0c;并在仪表盘上显示&#xff0c;如…

win系统微软输入法踩坑之输入法

例如&#xff1a;字母间距变宽 或者打字总是繁体等等 字母变宽是因为快捷键误触切换成了全角字符。。 立马打开输入法设置界面进行相关设置&#xff08;你要解决的输入法问题都可以在这里得到解决&#xff09;&#xff1a;

LED恒流驱动芯片:700V高压线性恒流选型一览表

LED恒流驱动芯片是一种专门用于LED照明产品的电子元件&#xff0c;它能够实现LED的恒流驱动&#xff0c;从而保证LED灯具在工作过程中稳定的亮度和电流输出。其中&#xff0c;700V高压线性恒流是LED恒流驱动芯片的一个重要特性&#xff0c;下面就让我们来详细了解一下。 SM2253…

Apple Unity Plugins 接入GameCenter 崩溃解决方案

目录 问题问题原因解决方案可直接使用的UnityPlugins 问题 调用 GKLocalPlayer.Local.FetchItems() 程序崩溃&#xff0c;报错&#xff1a;Thread 1: EXC_BAD_ACCESS (code257, address0x8000000000000002) 启动崩溃&#xff0c;报错&#xff1a;Library not loaded: rpath/Ap…

新年新计划,羊大师教你如何实现个人目标与成长

新年新计划&#xff0c;羊大师教你如何实现个人目标与成长 新的一年已经到来&#xff0c;这是一个神奇的时刻&#xff0c;一个全新的开始&#xff0c;也是实现自己目标与成长的最佳时机。在这个瞬息万变的世界中&#xff0c;我们总是被忙碌的生活所迷失&#xff0c;然而我们需…