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

黑马点评-02使用Redis代替session,Redis + token机制实现

Redis代替session

session共享问题

每个Tomcat中都有一份属于自己的session,所以多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时可能会导致数据丢失

用户第一次访问1号tomcat并把自己的信息存放session域中, 如果第二次访问到了2号tomcat就无法获取到在1号服务器存放的信息,导致登录拦截功能会出问题

session拷贝: 每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session实现session的共享

  • 每台服务器中都有完整的一份session数据导致服务器压力过大
  • session拷贝数据时,可能会出现延迟

使用Redis替换session可以实现服务器共享数据的问题

  • redis的三大特点: 数据共享(所有的服务器都可以在里面查询数据),内存存储(高性能),键值对的存储结构
    在这里插入图片描述

存储用户信息key的结构

如果存入的数据(登陆用户的信息)比较简单,可以考虑使用Redis的String或hash结构存储数据

  • String结构: 使用JSON字符串来保存登录的用户信息,信息比较直观但不易修改数据, 并且会额外的存储一些字符占用内存

在这里插入图片描述

  • Hash结构: 将对象中的每个属性独立存储,既可以针对单个字段做CRUD, 内存占用少只会存储数据本身不用保存序列化对象信息或者JSON的一些额外字符串

在这里插入图片描述

基于Redis实现短信登录

Redis的key是共享的,key要具有唯一性避免其他服务器在存储数据的时候出现key重复value覆盖的问题,用户发起请求时key还要方便携带

在这里插入图片描述

第一步: 修改sendCode方法,验证码不再保存到session中而是保存到Redis中(手机号作为key),验证码需要设置一个有效期节省Redsi内存

 public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;
// 自动注入StringRedisTemplate客户端操作Redis
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) throws MessagingException {// 验证邮箱的格式if (RegexUtils.isEmailInvalid(phone)) {return Result.fail("邮箱格式不正确");}// 生成验证码并保存到Redis,执行set key value ex 120String code = MailUtils.achieveCode();stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);log.info("发送登录验证码:{}", code);// 发送验证码,注意子类继承的方法不能比父类抛出更多的异常MailUtils.sendTestMail(phone, code);return Result.ok();
}

第二步: 修改login方法,将验证码和用户信息都存储到Redis中

  • 存储验证码时不再存储到session中,而是将手机号作为key存储到Redis的String结构中,校验验证码时从Redis中获取验证码
  • 存储用户信息时不再是存储到session中,而是随机生成一个token(登录令牌)作为key,将用户信息转换为HashMap对象存储到Redis的Hash结构中
  • 使用StringRedisTemplate向Redis中存数据时要求key和value都是String类型,所以将对象的每个属性转换成Map集合的元素时要求key和value都是String类型
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 30L;
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.从redis获取验证码并和用户提交的验证码校验比对String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 不一致则报错return Result.fail("验证码错误");}// 4.验证码一致则根据手机号查询用户select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 5.判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7.保存用户信息到redis中// 7.1.随机生成token作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2.将UserDTO对象的属性转为HashMap集合中的元素,这样一次可以存储多个键值对UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 手动将UserDTO的每个属性及其值都转化为String类型并存储到HashMap集合HashMap<String, String > userMap = new HashMap<>();userMap.put("icon", userDTO.getIcon());userMap.put("id", String.valueOf(userDTO.getId()));userMap.put("nickName", userDTO.getNickName());// 使用万能工具类将userDTO对象的属性转化为HashMap集合中的元素,创建CopyOptions用来自定义key和value的类型    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3.调用putAll方法将HashMap集合存储到Redis当中String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.设置tokenKey有效期为30分钟stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 7.5 登陆成功则删除验证码信息stringRedisTemplate.delete(LOGIN_CODE_KEY + phone);// 8.返回tokenreturn Result.ok(token);
}

第三步:前端将后端返回的token保存到浏览器当中

login(){if(!this.radio){this.$message.error("请先确认阅读用户协议!");return}if(!this.form.phone || !this.form.code){this.$message.error("手机号和验证码不能为空!");return}axios.post("/user/login", this.form).then(({data}) => { // data是后端随机生成的tokenif(data){// 保存taken到浏览器当中sessionStorage.setItem("token", data);}// 跳转到首页,info.html是用户详情页location.href = "/index.html"})
}// request拦截器,每次发请求都会将用户token放入请求头中
let token = sessionStorage.getItem("token");
axios.interceptors.request.use(config => {if(token) config.headers['authorization'] = tokenreturn config},
)

登录状态刷新问题

我们保存到Redis中的tokenKey的有效期为30分钟,在这段时间内根据用户有无操作决定是否刷新tokenKey的有效期

  • 如果用户有操作就需要刷新tokenKey的有效期,如果用户没有任何操作30分钟后tokenKey会消失,此时登录校验时就无法获取登录用户的信息,用户需要重新登录

在这里插入图片描述

第一步: 修改登陆拦截器,通过拦截器拦截到的请求来证明用户是否在操作

public class LoginInterceptor implements HandlerInterceptor {//@Autowired,这里不能自动装配,因为LoginInterceptor是我们手动在WebConfig里new出来的并不受容器的管理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");//2. 如果token是空表示未登录需要拦截if (StrUtil.isBlank(token)) {response.setStatus(401);return false;}//3. 基于token作为key获取Redis的Hash结构中保存的用户数据,返回的是一个Map集合String key = RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//4. 判断Map集合中有没有元素,没有则拦截if (userMap.isEmpty()) {response.setStatus(401);return false;}//5. 将查询到的Hash结构的数据转化为UserDto对象,fasle表示不忽略转化时的错误 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//6. 将用户信息保存到UserHolder类的ThreadLocal中UserHolder.saveUser(userDTO);//7. 刷新token有效期为30分钟stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//8. 放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

第二步: 在配置类MvcConfig注册拦截器使其生效

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册登录拦截器registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns(// 排除不需要拦截的路径"/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**");}
}

登陆状态刷新优化

LoginInterceptor拦截器只能拦截需要登陆校验的路径,若当前用户访问了一些不会被拦截的路径,此时登录拦截器就不会生效,那么令牌刷新动作也就不会执行

在这里插入图片描述

第一步: 编写RefreshTokenInterceptor拦截器拦截所有路径: 负责基于token获取用户信息,然后保存用户的信息到ThreadLocal当中,同时刷新令牌的有效期

public class RefreshTokenInterceptor implements HandlerInterceptor {// 这里并不是自动装配,因为RefreshTokenInterceptor是我们手动在WebConfig里new出来的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()) {// 用户不存在也放行交给LoginInterceptor处理return true;}// 5.将从Redis中查询到的Hash结构的数据转为UserDTO对象,fasle表示不忽略转化时的错误UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.将用户信息保存到UserHolder类的ThreadLocal中UserHolder.saveUser(userDTO);// 7.刷新token有效期为30分钟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();}
}

第二步: 修改LoginInterceptor登陆拦截器只负责登录校验,即只需要判断UserHolder类的ThreadLocal中是否存在用户信息,存在放行不存在则拦截

public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断UserHolder类的ThreadLocal中是否有用户if (UserHolder.getUser() == null) {// 用户信息不存在则拦截并设置状态码response.setStatus(401);return false;}// 用户信息存在则放行return true;}
}

第二步: 在配置类MvcConfig注册两个拦截器,并设置它们的执行顺序和拦截路径

  • 拦截器的执行顺序默认按照添加的顺序执行,但也可以由order来指定顺序(数字越小优先级越高),另外如果拦截器未设置拦截路径则默认是拦截所有路径
@Configuration
public class MvcConfig implements WebMvcConfigurer {//自动装配@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**").order(1);        // 注册刷新token的拦截器,RefreshTokenInterceptor是我们手动new出来的,只能通过构造方法为其注入StringRedisTemplate//registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);}
}

相关文章:

黑马点评-02使用Redis代替session,Redis + token机制实现

Redis代替session session共享问题 每个Tomcat中都有一份属于自己的session,所以多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时可能会导致数据丢失 用户第一次访问1号tomcat并把自己的信息存放session域中, 如果第二次访问到了2号tomcat就无法获取到在1号…...

arm 点灯实验代码以及现象

.text .global _start _start: 1.设置GPIOE寄存器的时钟使能 RCC_MP_AHB4ENSETR[4]->1 0x50000a28 LDR R0,0x50000A28 LDR R1,[R0] ORR R1,R1,#(0x1<<4) 第4位置1 STR R1,[R0] 1.设置GPIOF寄存器的时钟使能 RCC_MP_AHB4ENSETR[4]->1 0x50000a28 LDR R…...

选择适合普通公司的项目管理软件

不管是打工人还是学生党都适合使用Zoho Projects项目管理软件。利用项目概览功能&#xff0c;将整体项目尽收眼底&#xff0c;作为项目管理者&#xff0c;项目日程、进度都可见&#xff0c;Zoho Projects项目管理APP助推项目每一环节的进展&#xff0c;更便于管理者设计项目的下…...

E (1081) : DS堆栈--逆序输出(STL栈使用)

Description C中已经自带堆栈对象stack&#xff0c;无需编写堆栈操作的具体实现代码。 本题目主要帮助大家熟悉stack对象的使用&#xff0c;然后实现字符串的逆序输出 输入一个字符串&#xff0c;按字符按输入顺序压入堆栈&#xff0c;然后根据堆栈后进先出的特点&#xff0…...

访问者模式 行为型设计模式之九

1.定义 在不改变数据结构的前提下&#xff0c;增加作用于一组对象元素的新功能。 2.动机 访问者模式适用于数据结构相对稳定的系统它把数据结构和作用于数据结构之上的操作之间的耦合解脱开&#xff0c;使得操作集合可以相对自由的演化。访问者模式的目的是要把处理从数据结构…...

JVM垃圾回收之JVM GC算法探究

JVM垃圾回收之JVM GC算法探究 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;垃圾回收&#xff08;Garbage Collection&#xff0c;GC&#xff09;是自动管理内存的重要机制&#xff0c;它负责回收程序中不再使用的对象所占用的内存。GC算法是垃圾回收的核心&#xf…...

Django 前端模板显示换行符、日期格式

linebreaksbr 显示换行符 <td>{{ data.sku_list|default:"无"|linebreaksbr }}</td> date:"Y年m月d日 H:i" 设置日期格式 <td>{{ data.submit_time|date:"Y年m月d日 H:i" }}</td> 其他语法 forloop 获取循环的索引 …...

Aurora中的策略模式和模板模式

Aurora中的策略模式和模板模式 在aurora中为了方便以后的扩展使用了策略模式和模板模式实现图片上传和搜索功能&#xff0c;能够在配置类中设置使用Oss或者minio上传图片&#xff0c;es或者mysql文章搜索。后续有新的上传方式或者搜索方式只需要编写对应的实现类即可&#xff…...

Ubuntu 22.04 安装系统 手动分区 针对只有一块硬盘 lvm 单独分出/home

自动安装的信息 参考自动安装时产生的分区信息 rootyeqiang-MS-7B23:~# fdisk /dev/sdb -l Disk /dev/sdb&#xff1a;894.25 GiB&#xff0c;960197124096 字节&#xff0c;1875385008 个扇区 Disk model: INTEL SSDSC2KB96 单元&#xff1a;扇区 / 1 * 512 512 字节 扇区大…...

Android系统定制之监听USB键盘来判断是否弹出软键盘

一.项目背景 在设备上弹出软键盘,会将一大部分UI遮挡起来,造成很多图标无法看到和点击,使用起来不方便,因此通过插入usb键盘输入代替软键盘,但是点击输入框默认会弹出软键盘,因此想要插入USB键盘时,默认关闭软键盘,拔出键盘时再弹出,方便用户使用 二.设计思路 2.1…...

LeakyReLU激活函数

nn.LeakyReLU 是PyTorch中的Leaky Rectified Linear Unit&#xff08;ReLU&#xff09;激活函数的实现。Leaky ReLU是一种修正线性单元&#xff0c;它在非负数部分保持线性&#xff0c;而在负数部分引入一个小的斜率&#xff08;通常是一个小的正数&#xff09;&#xff0c;以防…...

Qt单一应用实例判断

原本项目中使用QSharedMemory的方法来判断当前是否已存在运行的实例&#xff0c;但在MacOS上&#xff0c;当程序异常崩溃后&#xff0c;QSharedMemory没有被正常销毁&#xff0c;导致应用程序无法再次被打开。 对此&#xff0c;Qt assistant中有相关说明&#xff1a; 摘抄 qt-s…...

企业AI工程化之路:如何实现高效、低成本、高质量的落地?

MLOps工程实践 概述面临挑战目的内容简介读者对象专家推荐目录 写在末尾&#xff1a; 主页传送门&#xff1a;&#x1f4c0; 传送 概述 作为计算机科学的一个重要领域&#xff0c;机器学习也是目前人工智能领域非常活跃的分支之一。机器学习通过分析海量数据、总结规律&#x…...

最短路径专题8 交通枢纽 (Floyd求最短路 )

题目&#xff1a; 样例&#xff1a; 输入 4 5 2 0 1 1 0 2 5 0 3 3 1 2 2 2 3 4 0 2 输出 0 7 思路&#xff1a; 由题意&#xff0c;绘制了该城市的地图之后&#xff0c;由给出的 k 个编号作为起点&#xff0c;求该点到各个点之间的最短距离之和最小的点是哪个&#xff0c;并…...

文件扫描模块

文章目录 前言文件扫描模块设计初级扫描方案一实现单线程扫描整合扫描步骤 设计初级扫描方案二周期性扫描 总结 前言 我们这个模块考虑的是数据库里面的内容从哪里获取。 获取完成后&#xff0c;这时候,我们就需要把目录里面文件/子文件都获取出来,并存入数据库。 文件扫描模…...

MySQL之主从复制

概述&#xff1a; 将主库的数据 变更同步到从库&#xff0c;从而保证主库和从库数据一致。 它的作用是 数据备份&#xff0c;失败迁移&#xff0c;读写分离&#xff0c;降低单库读写压力 原理&#xff1a; 主服务器上面的任何修改都会保存在二进制日志&#xff08; Bin-log日志…...

[leetcode 单调栈] 901. 股票价格跨度 M

设计一个算法收集某些股票的每日报价&#xff0c;并返回该股票当日价格的 跨度 。 当日股票价格的 跨度 被定义为股票价格小于或等于今天价格的最大连续日数&#xff08;从今天开始往回数&#xff0c;包括今天&#xff09;。 例如&#xff0c;如果未来 7 天股票的价格是 [100…...

Java线程池:并发编程的利器

Java线程池&#xff1a;并发编程的利器 在多任务、高并发的时代&#xff0c;Java并发编程显得尤为重要。其中&#xff0c;Java线程池是一种高效的管理线程的工具&#xff0c;能够提高应用程序的性能和响应速度。本文将深入探讨Java线程池的工作原理、应用场景以及简单示例&…...

ARM硬件断点

hw_breakpoint 是由处理器提供专门断点寄存器来保存一个地址&#xff0c;是需要处理器支持的。处理器在执行过程中会不断去匹配&#xff0c;当匹配上后则会产生中断。 内核自带了硬件断点的样例linux-3.16\samples\hw_breakpoint\data_breakpoint.c static void sample_hbp_h…...

Java使用WebSocket(基础)

准备一个html页面 <!DOCTYPE HTML> <html> <head><meta charset"UTF-8"><title>WebSocket Demo</title> </head> <body><input id"text" type"text" /><button onclick"send()&…...

python如何将word的doc另存为docx

将 DOCX 文件另存为 DOCX 格式&#xff08;Python 实现&#xff09; 在 Python 中&#xff0c;你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是&#xff0c;.doc 是旧的 Word 格式&#xff0c;而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...

Mysql中select查询语句的执行过程

目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析&#xff08;Parser&#xff09; 2.4、执行sql 1. 预处理&#xff08;Preprocessor&#xff09; 2. 查询优化器&#xff08;Optimizer&#xff09; 3. 执行器…...

视觉slam十四讲实践部分记录——ch2、ch3

ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...

webpack面试题

面试题&#xff1a;webpack介绍和简单使用 一、webpack&#xff08;模块化打包工具&#xff09;1. webpack是把项目当作一个整体&#xff0c;通过给定的一个主文件&#xff0c;webpack将从这个主文件开始找到你项目当中的所有依赖文件&#xff0c;使用loaders来处理它们&#x…...

【1】跨越技术栈鸿沟:字节跳动开源TRAE AI编程IDE的实战体验

2024年初&#xff0c;人工智能编程工具领域发生了一次静默的变革。当字节跳动宣布退出其TRAE项目&#xff08;一款融合大型语言模型能力的云端AI编程IDE&#xff09;时&#xff0c;技术社区曾短暂叹息。然而这一退场并非终点——通过开源社区的接力&#xff0c;TRAE在WayToAGI等…...

k8s从入门到放弃之Pod的容器探针检测

k8s从入门到放弃之Pod的容器探针检测 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;容器探测是指kubelet对容器执行定期诊断的过程&#xff0c;以确保容器中的应用程序处于预期的状态。这些探测是保障应用健康和高可用性的重要机制。Kubernetes提供了两种种类型…...

华为OD机考- 简单的自动曝光/平均像素

import java.util.Arrays; import java.util.Scanner;public class DemoTest4 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint[] arr Array…...

CMake系统学习笔记

CMake系统学习笔记 基础操作 最基本的案例 // code #include <iostream>int main() {std::cout << "hello world " << std::endl;return 0; }// CMakeLists.txt cmake_minimum_required(VERSION 3.0)# 定义当前工程名称 project(demo)add_execu…...

论文阅读笔记——Large Language Models Are Zero-Shot Fuzzers

TitanFuzz 论文 深度学习库&#xff08;TensorFlow 和 Pytorch&#xff09;中的 bug 对下游任务系统是重要的&#xff0c;保障安全性和有效性。在深度学习&#xff08;DL&#xff09;库的模糊测试领域&#xff0c;直接生成满足输入语言(例如 Python )语法/语义和张量计算的DL A…...