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

news2024/11/20 14:37:41

👨‍🎓作者简介:一位大四、研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
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public 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方法:

	@Override
    public 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

        //返回token
        return Result.ok(token);
    }

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

	private StringRedisTemplate stringRedisTemplate;

    //这里只能自己写构造函数来注入,没办法用@autowired注解,因为LoginInterceptor的对象是手动new出来的
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头中的token
        String 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);
        //存在,保存用户信息到ThreadLocal
        UserHolder.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;
    }


    @Override
    public 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);
        //存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //刷新token的有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //放行
        return true;
    }

    @Override
    public 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 {

    @Override
    public 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 {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //添加拦截器
    @Override
    public 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.coloradmin.cn/o/1358362.html

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

相关文章

Ambiq推出语音增强人工智能以消除物联网应用中的噪声

超低功耗半导体解决方案供应商Ambiq推出了其最新产品——神经网络语音增强器 (NNSE)&#xff0c;并已将该方案加入到neuralSPOT的&#xff08;开源模型&#xff09;Model Zoo中。这一高度优化过的AI模型可以高效实时地将背景噪声从设备对话中去除&#xff0c;从而在嘈杂的环境中…

基于Kettle开发的web版数据集成开源工具(data-integration)-应用篇

目录 &#x1f4da;第一章 基本流程梳理&#x1f4d7;页面基本操作&#x1f4d7;对应后台服务流程 &#x1f4da;第二章 二开思路&#x1f4d7;前端&#x1f4d7;后端 &#x1f53c;上一集&#xff1a;基于Kettle开发的web版数据集成开源工具(data-integration)-介绍篇 *️⃣主…

对话小仙炖副总裁张勇:内容价值将成为直播电商的核心趋势和竞争力

“ 激活中医典籍里的智慧&#xff0c;坚持内容化之路&#xff0c;服务好消费者。” 整理 | 飞族 编辑 | 渔舟 出品&#xff5c;极新&#xff06;北京电子商务协会 随着直播电商的影响力越来越大&#xff0c;对品牌而言&#xff0c;直播不仅是一种单纯的卖货渠道&#xff0c;…

Open3D聚类算法

按照官网的例子使用聚类&#xff0c;发现结果是全黑的。 经过多次测试发现 eps3.3, min_points1这里是关键 min_points必须等于1否则无效果 import time import open3d as o3d; import numpy as np; import matplotlib.pyplot as plt#坐标 mesh_coord_frame o3d.geometry.Tria…

力扣题:高精度运算-1.3

力扣题-1.3 [力扣刷题攻略] Re&#xff1a;从零开始的力扣刷题生活 力扣题1&#xff1a;43. 字符串相乘 解题思想&#xff1a;类似计算时采用的竖式乘法。首先取得num2的低位&#xff0c;并补齐对应的0&#xff0c;然后与num1进行相乘&#xff0c;然后进行字符串的相加操作。…

使用 pg_stat_statements 优化查询

使用 pg_stat_statements 优化查询 就使用量和社区规模而言&#xff0c;PostgreSQL 是增长最快的数据库之一&#xff0c;得到许多专业开发人员的支持&#xff0c;并得到广泛的工具、连接器、库和可视化应用程序生态系统的支持。 PostgreSQL 也是可扩展的&#xff1a;使用 Postg…

实现HSRP-热备份路由协议

实现HSRP-热备份路由协议 <HSRP多组实现> 网络工程师必会的企业网络常用双机热备协议之HSRP。 实验拓扑: 实验目的: 通过配置多组HSRP实现网关自动切换和链接负载均衡,既当网络正常时PC1,PC3通过R1到达R3,PC2,PC4通过R2到达R3,当R1或R2发生故障时网关能自动切换,以确…

企业微信开发:自建应用:获取企业微信IP段(用于防火墙配置)

概述 在企业微信开发流程中&#xff0c;为了确保与企业微信API的网络通信安全&#xff0c;并适应防火墙配置要求&#xff0c;开发者需要获取企业微信API服务的IP地址范围。这样&#xff0c;仅允许与企业微信官方通信的合法请求通过防火墙&#xff0c;从而保障数据传输的安全性…

教学目标是什么

教学目标&#xff0c;作为教学活动的灵魂之所在&#xff0c;对于教育者和学生都至关重要。然而&#xff0c;你是否曾对此产生过疑问&#xff1a;教学目标究竟是什么&#xff1f;它又如何影响我们的教学活动呢&#xff1f; 教学目标就像一座灯塔&#xff0c;为教学活动指明方向&…

MapInfo Pro和Python基础知识

MapInfo Pro用户长期以来一直使用MapBasic脚本语言来自动化任务、构建自定义应用程序、创建Pro的特定领域自定义、将Pro与其他工具集成等。 MapBasic主要是一种编译语言&#xff0c;这对非程序员来说有点障碍。 我们确实有MapBasic窗口&#xff0c;它允许MapBasic语句和代码直接…

罗德与施瓦茨FSVA40信号和频谱分析仪

罗德与施瓦茨FSVA40是一款功能信号和频谱分析仪&#xff0c;适用于从事射频系统的开发、生产、安装和服务的用户。FSVA40信号和频谱分析仪系列始终提供最佳的价格和性能组合&#xff0c;无论是根据最新通信标准测试生产中的无线设备&#xff0c;还是测量低相位噪声、高灵敏度和…

根本记不住MySQL进阶查询语句

1 MySQL进阶查询 1.1 MySQL进阶查询的语句 全文以数据库location和Store_Info为实例 ---- SELECT ----显示表格中一个或数个字段的所有数据记录 语法&#xff1a;SELECT "字段" FROM "表名"; select 列名 from 表名 ; ---- DISTINCT ----不显示重复的数…

农业银行RPA实践 3大典型案例分析

零接触开放金融服务在疫情之下被越来越多的银行和客户所认同&#xff0c;引起了更广泛的持续关注&#xff0c;各家银行纷纷开展产品服务创新&#xff0c;加速渠道迁移&#xff0c;同时通过远程办公、构建金融生态等方式积极推进零接触开放金融体系建设。 随着商业银行科技力量的…

后端开发——JDBC的学习(三)

本篇继续对JDBC进行总结&#xff1a; ①通过Service层与Dao层实现转账的练习&#xff1b; ②重点&#xff1a;由于每次使用连接就手动创建连接&#xff0c;用完后就销毁&#xff0c;这样会导致资源浪费&#xff0c;因此引入连接池&#xff0c;练习连接池的使用&#xff1b; …

新年福利|这款价值数万的报表工具永久免费了

随着数据资产的价值逐渐凸显&#xff0c;越来越多的企业会希望采用报表工具来处理数据分析&#xff0c;了解业务经营状况&#xff0c;从而辅助经营决策。不过&#xff0c;企业在选型报表工具的时候经常会遇到以下几个问题&#xff1a; 各个报表工具有很多功能和特性&#xff0c…

Python数据处理库之tablib详解

概要 Python 提供了许多库和工具来处理数据&#xff0c;其中之一就是 tablib。tablib 是一个功能强大且易于使用的库&#xff0c;用于处理各种数据格式&#xff0c;包括Excel、CSV、JSON等。它不仅可以用于数据导入和导出&#xff0c;还支持数据转换、过滤、合并等操作。本文将…

《C++语言程序设计(第5版)》(清华大学出版社,郑莉 董渊编著)习题——第2章 C++语言简单程序设计

2-28 用穷举法找出1~100的质数并显示出来。分别使用while、do-while、for循环语句实现。 // 使用while循环#include <iostream>using namespace std;int main() {int number 2;cout << "1~100之间的质数有&#xff1a;";while (number < 100) {int …

HarmonyOS应用开发之ArkTS语言学习记录

1、ArkTS介绍 ArkTS是鸿蒙生态的应用开发语言。它在保持TypeScript&#xff08;简称TS&#xff09;基本语法风格的基础上&#xff0c;对TS的动态类型特性施加更严格的约束&#xff0c;引入静态类型。同时&#xff0c;提供了声明式UI、状态管理等相应的能力&#xff0c;让开发者…

Xcode15在iOS12系统上崩溃的原因

1.1.崩溃在_dyld_start&#xff0c;如图&#xff1a; 崩溃截图 解决办法&#xff1a;在other link flags添加-ld64 注意&#xff1a;该办法只能解决运行真机&#xff0c;archive出来的包依然报错闪退...... 1.2 SwiftUI导致iOS12及以下系统闪退问题 SwiftUI是iOS13开始使用&…

MongoDB 启动时:服务名无效

1.问题场景 电脑睡眠后&#xff0c;再连接服务发现无法连接&#xff0c;启动服务报&#xff1a;服务名无效。 2.打开服务管理&#xff1a; 发现服务中没有MongoDB的服务 3.解决 &#xff08;1&#xff09;先找打MongoDB安装路径&#xff0c;把data文件夹下所有文件删除 &a…