基于Redis实现短信验证码登录

news2025/1/24 17:38:52

目录

1 基于Session实现短信验证码登录

2 配置登录拦截器

3 配置完拦截器还需将自定义拦截器添加到SpringMVC的拦截器列表中 才能生效

4 Session集群共享问题

5 基于Redis实现短信验证码登录

6 Hash 结构与 String 结构类型的比较

7 Redis替代Session需要考虑的问题

8 基于redis短信验证登录

9 配置登录拦截器


1 基于Session实现短信验证码登录

    /**
     * 发送验证码
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1、判断手机号是否合法
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式不正确");
        }
        // 2、手机号合法,生成验证码,并保存到Session中
        String code = RandomUtil.randomNumbers(6);
        session.setAttribute(SystemConstants.VERIFY_CODE, code);
        // 3、发送验证码
        log.info("验证码:{}", code);
        return Result.ok();
    }

    /**
     * 用户登录
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        // 1、判断手机号是否合法
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式不正确");
        }
        // 2、判断验证码是否正确
        String sessionCode = (String) session.getAttribute(LOGIN_CODE);
        if (code == null || !code.equals(sessionCode)) {
            return Result.fail("验证码不正确");
        }
        // 3、判断手机号是否是已存在的用户
        User user = this.getOne(new LambdaQueryWrapper<User>()
                .eq(User::getPassword, phone));
        if (Objects.isNull(user)) {
            // 用户不存在,需要注册
            user = createUserWithPhone(phone);
        }
        // 4、保存用户信息到Session中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
        session.setAttribute(LOGIN_USER, user);
        return Result.ok();
    }

    /**
     * 根据手机号创建用户
     */
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        this.save(user);
        return user;
    }

2 配置登录拦截器

public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 前置拦截器,用于判断用户是否登录
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        // 1、判断用户是否存在
        User user = (User) session.getAttribute(LOGIN_USER);
        if (Objects.isNull(user)){
            // 用户不存在,直接拦截
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        // 2、用户存在,则将用户信息保存到ThreadLocal中,方便后续逻辑处理
        // 比如:方便获取和使用用户信息,session获取用户信息是具有侵入性的
        ThreadLocalUtls.saveUser(user);

        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
}

3 配置完拦截器还需将自定义拦截器添加到SpringMVC的拦截器列表中 才能生效

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                // 设置放行请求
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

4 Session集群共享问题

(1)什么是Session集群共享问题

在分布式集群环境中,会话(Session)共享是一个常见的挑战。默认情况下,Web 应用程序的会话是保存在单个服务器上的,当请求不经过该服务器时,会话信息无法被访问。

(2) Session集群共享问题造成哪些问题

 服务器之间无法实现会话状态的共享。比如:在当前这个服务器上用户已经完成了登录,Session中存储了用户的信息,能够判断用户已登录,但是在另一个服务器的Session中没有用户信息,无法调用显示没有登录的服务器上的服务

 (3)如何解决Session集群共享问题

方案一:Session拷贝(不推荐)

Tomcat提供了Session拷贝功能,通过配置Tomcat可以实现Session的拷贝,但是这会增加服务器的额外内存开销,同时会带来数据一致性问题

方案二:Redis缓存(推荐)

Redis缓存具有Session存储一样的特点,基于内存、存储结构可以是key-value结构、数据共享

(4)Redis缓存相较于传统Session存储的优点

1 高性能和可伸缩性:Redis 是一个内存数据库,具有快速的读写能力。相比于传统的 Session 存储方式,将会话数据存储在 Redis 中可以大大提高读写速度和处理能力。此外,Redis 还支持集群和分片技术,可以实现水平扩展,处理大规模的并发请求。
2 可靠性和持久性:Redis 提供了持久化机制,可以将内存中的数据定期或异步地写入磁盘,以保证数据的持久性。这样即使发生服务器崩溃或重启,会话数据也可以被恢复。
3 丰富的数据结构:Redis 不仅仅是一个键值存储数据库,它还支持多种数据结构,如字符串、列表、哈希、集合和有序集合等。这些数据结构的灵活性使得可以更方便地存储和操作复杂的会话数据。
4 分布式缓存功能:Redis 作为一个高效的缓存解决方案,可以用于缓存会话数据,减轻后端服务器的负载。与传统的 Session 存储方式相比,使用 Redis 缓存会话数据可以大幅提高系统的性能和可扩展性。
5 可用性和可部署性:Redis 是一个强大而成熟的开源工具,有丰富的社区支持和活跃的开发者社区。它可以轻松地与各种编程语言和框架集成,并且可以在多个操作系统上运行。
PS:但是Redis费钱,而且增加了系统的复杂度

5 基于Redis实现短信验证码登录

 

Hash 结构与 String 结构类型的比较

  • String 数据结构是以 JSON 字符串的形式保存,更加直观,操作也更加简单,但是 JSON 结构会有很多非必须的内存开销,比如双引号、大括号,内存占用比 Hash 更高
  • Hash 数据结构是以 Hash 表的形式保存,可以对单个字段进行CRUD,更加灵活

 7 Redis替代Session需要考虑的问题

(1)选择合适的数据结构,了解 Hash 比 String 的区别
(2)选择合适的key,为key设置一个业务前缀,方便区分和分组,为key拼接一个UUID,避免key冲突防止数据覆盖
(3)选择合适的存储粒度,对于验证码这类数据,一般设置TTL为3min即可,防止大量缓存数据的堆积,而对于用户信息这类数据可以稍微设置长一点,比如30min,防止频繁对Redis进行IO操作

 8 基于redis短信验证登录

    /**
     * 发送验证码
     *
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1、判断手机号是否合法
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式不正确");
        }
        // 2、手机号合法,生成验证码,并保存到Redis中
        String code = RandomUtil.randomNumbers(6);
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code,
                RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 3、发送验证码
        log.info("验证码:{}", code);
        return Result.ok();
    }

    /**
     * 用户登录
     *
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        // 1、判断手机号是否合法
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式不正确");
        }
        // 2、判断验证码是否正确
        String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (code == null || !code.equals(redisCode)) {
            return Result.fail("验证码不正确");
        }
        // 3、判断手机号是否是已存在的用户
        User user = this.getOne(new LambdaQueryWrapper<User>()
                .eq(User::getPhone, phone));
        if (Objects.isNull(user)) {
            // 用户不存在,需要注册
            user = createUserWithPhone(phone);
        }
        // 4、保存用户信息到Redis中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // 将对象中字段全部转成string类型,StringRedisTemplate只能存字符串类型的数据
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).
                        setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        String token = UUID.randomUUID().toString(true);
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        return Result.ok(token);
    }

    /**
     * 根据手机号创建用户并保存
     *
     * @param phone
     * @return
     */
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        this.save(user);
        return user;
    }

9 配置登录拦截器

单独配置一个拦截器用户刷新Redis中的token:在基于Session实现短信验证码登录时,我们只配置了一个拦截器,这里需要另外再配置一个拦截器专门用于刷新存入Redis中的 token,因为我们现在改用Redis了,为了防止用户在操作网站时突然由于Redis中的 token 过期,导致直接退出网站,严重影响用户体验。那为什么不把刷新的操作放到一个拦截器中呢,因为之前的那个拦截器只是用来拦截一些需要进行登录校验的请求,对于哪些不需要登录校验的请求是不会走拦截器的,刷新操作显然是要针对所有请求比较合理,所以单独创建一个拦截器拦截一切请求,刷新Redis中的Key

 登录拦截器:

public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 前置拦截器,用于判断用户是否登录
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断当前用户是否已登录
        if (ThreadLocalUtls.getUser() == null){
            // 当前用户未登录,直接拦截
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        // 用户存在,直接放行
        return true;
    }
}

刷新token的拦截器

public class RefreshTokenInterceptor implements HandlerInterceptor {

    // new出来的对象是无法直接注入IOC容器的(LoginInterceptor是直接new出来的)
    // 所以这里需要再配置类中注入,然后通过构造器传入到当前类中
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1、获取token,并判断token是否存在
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            // token不存在,说明当前用户未登录,不需要刷新直接放行
            return true;
        }
        // 2、判断用户是否存在
        String tokenKey = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
        if (userMap.isEmpty()){
            // 用户不存在,说明当前用户未登录,不需要刷新直接放行
            return true;
        }
        // 3、用户存在,则将用户信息保存到ThreadLocal中,方便后续逻辑处理,比如:方便获取和使用用户信息,Redis获取用户信息是具有侵入性的
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        ThreadLocalUtls.saveUser(BeanUtil.copyProperties(userMap, UserDTO.class));
        // 4、刷新token有效期
        stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }
}

将自定义的拦截器添加到SpringMVC的拦截器表中,使其生效:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // new出来的对象是无法直接注入IOC容器的(LoginInterceptor是直接new出来的)
    // 所以这里需要再配置类中注入,然后通过构造器传入到当前类中
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                // 设置放行请求
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1); // 优先级默认都是0,值越大优先级越低
        // 添加刷新token的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}
  • RefreshTokenInterceptor 先执行,主要用来检查 token 的有效性并刷新 token 的有效期,同时将用户信息存入 ThreadLocal
  • LoginInterceptor 后执行,验证 ThreadLocal 中是否有用户信息,以确认用户是否登录。

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

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

相关文章

校验收货地址是否超出配送范围实战3(day09)

优化用户下单功能&#xff0c;加入校验逻辑&#xff0c;如果用户的收货地址距离商家门店超出配送范围&#xff08;配送范围为5公里内&#xff09;&#xff0c;则下单失败。 提示&#xff1a; ​ 1. 基于百度地图开放平台实现&#xff08;https://lbsyun.baidu.com/&#xff09…

Vue2.0+ElementUI实现查询条件展开和收起功能组件

一、需求 el-form如果查询条件过多&#xff0c;影响页面的展示效果。查询条件表单是我们系统中非常常见的功能&#xff0c;我们需要把它封装成一个通用的组件&#xff0c;方便在系统开发中提升开发效率。除了在实现基本查询条件的功能上&#xff0c;还需要实现多条件的折叠和展…

UE求职Demo开发日志#8 强化前置条件完善,给物品加图标

1 强化前置条件完善 StrengthManager里实现一个Check前置的函数 bool CheckPreAllIsActive(int index)&#xff0c;所有的前置都已经激活就返回true&#xff0c;否则返回false 之后在强化的时候加入条件检查&#xff1a; 1.所有前置技能全部激活 2.本身没有强化过 最后测…

pinctrl子系统

目录 一、PinCtrl子系统的定义 二、明确PinCtrl子系统和我们编写驱动的关系 三、pinctrl_desc结构体引入 四、PinCtrl子系统驱动实现分析 1.芯片厂家是如何实现PinCtrl子系统的 2.linux在什么位置设置的引脚复用和电气属性 2.1 really_probe的主要功能 2.2 really_prob…

行政纠错——pycorrector学习

pycorrector是一个开源中文文本纠错工具&#xff0c;它支持对中文文本进行音似、形似和语法错误的纠正。此工具是使用Python3进行开发的&#xff0c;并整合了Kenlm、ConvSeq2Seq、BERT、MacBERT、ELECTRA、ERNIE、Transformer等多种模型来实现文本纠错功能。pycorrector官方仓库…

深入MapReduce——计算模型设计

引入 通过引入篇&#xff0c;我们可以总结&#xff0c;MapReduce针对海量数据计算核心痛点的解法如下&#xff1a; 统一编程模型&#xff0c;降低用户使用门槛分而治之&#xff0c;利用了并行处理提高计算效率移动计算&#xff0c;减少硬件瓶颈的限制 优秀的设计&#xff0c…

React+Cesium基础教程(001):创建基于React的Cesium项目及对Cesium进行基本配置

文章目录 01-基于react的cesium项目创建基于React的Cesium项目Cesium基本配置设置默认启动视角完整项目下载地址01-基于react的cesium项目 创建基于React的Cesium项目 创建react项目: create-react-app react-cesium-basic安装[cesium1.93.0]版本: npm install cesium@1.…

Vue2:使用sortablejs实现el-table中行拖拽调整顺序

如图,实现拖拽表格中的行来调整行顺序,但是其中的编号仍然是1、2、3、4的顺序,不跟着变化。 实现如下: 一、导入sortablejs import Sortable from "sortablejs";export default { components: {Sortable},data() {return {//数据中的id很重要,拖拽行重新排序…

Kingbase数据库体系结构和日常运维监控

1. 数据库架构 1.1. 内存 1.1.1. Share Memory 共享内存是服务器服务器为数据库缓存和事务日志缓存预留的内存缓存空间&#xff0c;其中最重要的组成部分是Shared Buffer和WAL Buffer&#xff1a; 1. Shared Buffer&#xff1a; 减少磁盘IO 2. WAL Buffer&#xff1a; 用…

【Prometheus】Prometheus如何监控Haproxy

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

【0x0012】HCI_Delete_Stored_Link_Key命令详解

目录 一、命令参数 二、命令格式及参数 2.1. HCI_Delete_Stored_Link_Key 命令格式 2.2. BD_ADDR 2.3. Delete_All 三、生成事件及参数 3.1. HCI_Command_Complete事件 3.2. Status 3.3. Num_Keys_Deleted 四、命令执行流程 4.1. 命令发送阶段 4.2. 控制器处理阶段…

github汉化

本文主要讲述了github如何汉化的方法。 目录 问题描述汉化步骤1.打开github&#xff0c;搜索github-chinese2.打开项目&#xff0c;打开README.md3.下载安装脚本管理器3.1 在README.md中往下滑动&#xff0c;找到浏览器与脚本管理器3.2 选择浏览器对应的脚本管理器3.2.1 点击去…

机器学习-K近邻算法

文章目录 一. 数据集介绍Iris plants dataset 二. 代码三. k值的选择 一. 数据集介绍 鸢尾花数据集 鸢尾花Iris Dataset数据集是机器学习领域经典数据集&#xff0c;鸢尾花数据集包含了150条鸢尾花信息&#xff0c;每50条取自三个鸢尾花中之一&#xff1a;Versicolour、Setosa…

C++函数——fill

在C中&#xff0c;std::fill 是标准库提供的一个算法适用于几乎所有类型的容器&#xff0c;只要这些容器支持迭代器操作。具体来说&#xff0c;std::fill 的适用性取决于容器是否提供了满足其要求的迭代器类型&#xff0c;用于将指定范围内的所有元素设置为某个特定值。它是一个…

如何打造高效同城O2O平台?外卖跑腿系统源码选型与开发指南

打造一个高效的同城O2O平台&#xff0c;选择合适的外卖跑腿系统源码并制定科学的开发方案至关重要。本篇文章&#xff0c;笔者将从源码选型、核心功能、开发架构及运营策略等方面&#xff0c;全面解析O2O平台的搭建思路。 一、外卖跑腿系统源码的选型要点 在搭建O2O平台时&…

Flutter_学习记录_基本组件的使用记录

1.TextWidge的常用属性 1.1TextAlign: 文本对齐属性 常用的样式有&#xff1a; TextAlign.center 居中TextAlign.left 左对齐TextAlign.right 有对齐 使用案例&#xff1a; body: Center(child: Text(开启 TextWidget 的旅程吧&#xff0c;珠珠, 开启 TextWidget 的旅程吧&a…

Qt实践:一个简单的丝滑侧滑栏实现

Qt实践&#xff1a;一个简单的丝滑侧滑栏实现 笔者前段时间突然看到了侧滑栏&#xff0c;觉得这个抽屉式的侧滑栏非常的有趣&#xff0c;打算这里首先尝试实现一个简单的丝滑侧滑栏。 首先是上效果图 &#xff08;C&#xff0c;GIF帧率砍到毛都不剩了&#xff09; QProperty…

10. SpringCloud Alibaba Sentinel 规则持久化部署详细剖析

10. SpringCloud Alibaba Sentinel 规则持久化部署详细剖析 文章目录 10. SpringCloud Alibaba Sentinel 规则持久化部署详细剖析1. 规则持久化1.1 Nacos Server 配置中心-规则持久化实例 2. 最后&#xff1a; 1. 规则持久化 规则没有持久化的问题 如果 sentinel 流控规则没有…

SpringCloud微服务Gateway网关简单集成Sentinel

Sentinel是阿里巴巴开源的一款面向分布式服务架构的轻量级流量控制、熔断降级组件。Sentinel以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保护等多个维度来帮助保护服务的稳定性。 官方文档&#xff1a;https://sentinelguard.io/zh-cn/docs/introduction.html …

正则表达式以及Qt中的使用

目录 一、正则表达式 1、基本匹配&#xff1a; 2、元字符&#xff1a; 2.1 .运算符&#xff1a; 2.2 字符集&#xff1a; 2.3 重复次数&#xff1a; 2.4 量词{} 2.5 特征标群() 2.6 或运算符 2.7 \反斜线转码特殊字符 2.8 锚点 3、简写字符 4、零宽度断言 4.1 正…