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.hqwc.cn/news/313725.html

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

相关文章

图文证明 费马,罗尔,拉格朗日,柯西

图文证明 罗尔,拉格朗日,柯西 费马引理和罗尔都比较好证,不过多阐述,看图即可: 费马引理: 罗尔定理: 重点来证明拉格朗日和柯西 拉格朗日: 我认为不需要去看l(x)的那一行更好推: 详细的推理过程: 构造 h ( x ) f ( x ) − l ( x ) , 因为 a , b 两点为交点 , f ( a ) l ( …

2024年【黑龙江省安全员C证】考试及黑龙江省安全员C证找解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年黑龙江省安全员C证考试为正在备考黑龙江省安全员C证操作证的学员准备的理论考试专题&#xff0c;每个月更新的黑龙江省安全员C证找解析祝您顺利通过黑龙江省安全员C证考试。 1、【多选题】下列属于编制安全检查…

2024年总结的前端学习路线分享(学习导读)

勤学如春起之苗&#xff0c;不见其增&#xff0c;日有所长 。辍学如磨刀之石&#xff0c;不见其损&#xff0c;日有所亏。 在写上一篇 2023年前端学习路线 的时候&#xff0c;时间还在2023年初停留&#xff0c;而如今不知不觉时间已经悄然来到了2024年&#xff0c;回顾往昔岁月…

人机交互中信息数量与质量

在人机交互中&#xff0c;信息的数量和质量都是非常重要的因素。 信息的数量指的是交互过程中传递的信息的多少。信息的数量直接影响到交互的效率和效果&#xff0c;如果交互中传递的信息量太少&#xff0c;可能导致交互过程中的信息不足&#xff0c;用户无法得到想要的结果或者…

深度学习 | Transformer模型及代码实现

Transformer 是 Google 的团队在 2017 年提出的一种 NLP 经典模型&#xff0c;现在比较火热的 Bert 也是基于 Transformer。Transformer 模型使用了 Self-Attention 机制&#xff0c;不采用 RNN 的顺序结构&#xff0c;使得模型可以并行化训练&#xff0c;而且能够拥有全局信息…

合伙企业法关于合伙企业的要求

合伙协议可以载明合伙企业的经营期限和合伙人争议的解决方式。 合伙协议经全体合伙人签名、盖章后生效。合伙人依照合伙协议享有权利&#xff0c;承担责任。 经全体合伙人协商一致&#xff0c;可以修改或者补充合伙协议。 申请合伙企业设立登记&#xff0c;应当向企业登记机关提…

CEC2017(Python):麻雀搜索算法SSA求解CEC2017(提供Python代码)

一、CEC2017简介 参考文献&#xff1a; [1]Awad, N. H., Ali, M. Z., Liang, J. J., Qu, B. Y., & Suganthan, P. N. (2016). “Problem definitions and evaluation criteria for the CEC2017 special session and competition on single objective real-parameter numer…

大甩卖-(CWRU)轴承故障诊数据集和代码全家桶

Python-凯斯西储大学&#xff08;CWRU&#xff09;轴承数据解读与分类处理 Python轴承故障诊断 (一)短时傅里叶变换STFT Python轴承故障诊断 (二)连续小波变换CWT_pyts 小波变换 故障-CSDN博客 Python轴承故障诊断 (三)经验模态分解EMD_轴承诊断 pytorch-CSDN博客 Pytorch…

基于C#的机械臂欧拉角与旋转矩阵转换

欧拉角概述 机器人末端执行器姿态描述方法主要有四种&#xff1a;旋转矩阵法、欧拉角法、等效轴角法和四元数法。所以&#xff0c;欧拉角是描述机械臂末端姿态的重要方法之一。 关于欧拉角的历史&#xff0c;由来已久&#xff0c;莱昂哈德欧拉用欧拉角来描述刚体在三维欧几里…

IBM介绍?

IBM&#xff0c;全名国际商业机器公司&#xff08;International Business Machines Corporation&#xff09;&#xff0c;是一家全球知名的美国科技公司。它成立于1911年&#xff0c;总部位于美国纽约州阿蒙克市&#xff08;Armonk&#xff09;&#xff0c;是世界上最大的信息…

240101-5步MacOS自带软件无损快速导出iPhone照片

硬件准备&#xff1a; iphone手机Mac电脑数据线 操作步骤&#xff1a; Step 1: 找到并打开MacOS自带的图像捕捉 Step 2: 通过数据线将iphone与电脑连接Step 3&#xff1a;iphone与电脑提示“是否授权“&#xff1f; >>> “是“Step 4&#xff1a;左上角选择自己的设…

Redis:原理+项目实战——Redis实战1(session实现短信登录(并剖析问题))

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理速成项目实战——Redis的Java客户端 &#x1f4da;订阅专栏&#xff1a;Redis速成 希望文章对你们有所帮助…