【SpringBoot篇】详解基于Redis实现短信登录的操作

文章目录

  • 🥰前言
  • 🛸StringRedisTemplate
    • 🌹使用StringRedisTemplate
      • ⭐常用的方法
  • 🛸为什么我们要使用Redis代替Session进行登录操作
    • 🎆具体使用
      • ✨编写拦截器
      • ✨配置拦截器
      • 🌺基于Redis实现发送手机验证码操作
        • 🎈总体思路
        • 🎈具体步骤
      • 🎍基于Redis实现短信登录并注册的操作
        • 🎈总体思路
        • 🎈具体步骤

在这里插入图片描述

🥰前言

使用 Redis 进行登录适用于以下情况:

  • 分布式系统:
    当系统需要支持多个节点的分布式部署时,使用 Redis 存储登录信息能够更好地支持多节点间的共享和同步,确保用户的登录状态能够在整个系统中得到有效的传递和管理。
  • 高并发访问:
    面对大规模的并发访问,使用 Redis 可以提供更好的性能表现。Redis 是一个基于内存的高性能 Key-Value 数据库,能够更快速地读取和写入数据,因此适用于需要处理大量并发请求的场景。
  • 灵活的数据结构需求:
    如果系统需要根据业务需求选择最佳的数据结构,并且对存储和操作登录信息有更多的灵活性,那么使用 Redis 将会是一个不错的选择。Redis 支持多种数据类型的存储和操作,包括字符串、哈希表、列表、集合和有序集合等,能够满足不同的业务需求。
  • 需要持久化支持:
    如果系统需要对登录信息进行持久化存储,以防止数据丢失,Redis 的持久化功能可以很好地满足这一需求。

总的来说,使用 Redis 进行登录适用于需要支持分布式部署、面对高并发访问、有灵活的数据结构需求以及需要持久化支持的系统场景。通过合理地利用 Redis 的特性,可以更好地满足上述情况下的需求,提高系统的可扩展性、性能和稳定性。

虽然 Spring Boot 应用通常是单体应用,但是在实际运行中,我们也经常会遇到多个实例同时运行的情况,这时候就需要使用 Redis 进行分布式 Session 管理。

🛸StringRedisTemplate

StringRedisTemplate是Spring Data Redis提供的一个类,它是一个具体的对象,用于操作Redis数据库中的字符串类型数据。

StringRedisTemplate封装了Redis的操作,并提供了一系列方法来对Redis中的字符串进行读取、写入和删除操作。它是RedisTemplate的一个子类,专门用于处理字符串类型的数据。

🌹使用StringRedisTemplate

首先引入依赖,引入StringRedisTemplate的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

⭐常用的方法

StringRedisTemplate提供了多个方法来操作Redis中的字符串类型数据。下面是一些常用的方法:

  • opsForValue().set(key, value):将一个字符串类型的值value存储到Redis中,并指定键key。
  • opsForValue().get(key):根据键key获取对应的字符串类型的值。
  • opsForValue().increment(key, delta):将键key所对应的值增加delta,delta可以为负数。
  • opsForValue().size(key):获取值的长度。

🛸为什么我们要使用Redis代替Session进行登录操作

集群session存在共享问题,会导致数据丢失

  • 保存相同的数据,大家互相copy,会有内存空间的浪费
  • 我们copy数据的时候,是需要有一定的时间的,会有延迟,如果在这个延迟之内,如果有人来访问,仍然会造成数据不一致的情况
    请添加图片描述
    如果我们使用Redis的话。
  • Redis是在tomcat外面的存储,如果任意一台tomcat都能访问到Redis,可以实现数据共享,储存在Redis里面的数据,任何tomcat都可以看到,使用就不存在数据丢失的问题
  • Redis读写延迟非常低,方便进行内存存储
  • Redis是key-value结构

在这里插入图片描述

🎆具体使用

✨编写拦截器

在这里插入图片描述

RefreshTokenInterceptor.java
在这里插入图片描述

在拦截器中配置拦截操作

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;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String 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()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

在这里插入图片描述

刷新token的目的

用户每访问一次,这个token就会刷新一次,主要用户一直在操作,这个token就不会消失


但是如果仅仅拦截的是需要登录的路径,用户 访问 不需要登录 的路径 的时候(比如首页),这个拦截器就不生效,此时token就不会刷新,这样子,过了token的有效期后,尽管用户还在访问,用户的登录状态却消失了,这样肯定不太合理

那么我们就需要在原来的拦截器基础上再加上一个拦截器
请添加图片描述

LoginInterceptor.java
在这里插入图片描述

在拦截器中配置拦截操作

package com.hmdp.utils;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}

在这里插入图片描述

✨配置拦截器

我们上面编写了拦截器,我们还需要配置拦截器,使这个拦截器生效

MvcConfig.java
这里是引用

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(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

在这里插入图片描述

🌺基于Redis实现发送手机验证码操作

🎈总体思路

在这里插入图片描述

🎈具体步骤

我们首先引入上面说的依赖,然后在application.yml文件(或yaml文件)中进行配置,如下
在这里插入图片描述

下面我们编写发送手机验证码的核心代码

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5.发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }
}

在这里插入图片描述

上面代码里面的RegexUtils.isPhoneInvalid(phone)这段代码是什么用法

在这里插入图片描述

stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);这段代码有什么用

这段代码的作用是将一个验证码(即code)存储到Redis中,并设置了过期时间为LOGIN_CODE_TTL分钟。以便在一定时间后自动删除该键值对。

🎍基于Redis实现短信登录并注册的操作

🎈总体思路

在这里插入图片描述

🎈具体步骤

我们首先引入上面说的依赖,并且在application.yml文件(或yaml文件)中进行配置(同上)
然后我们来编写核心代码

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @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.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);
    }
    
    private User createUserWithPhone(String phone) {
        // 1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        // 2.保存用户
        save(user);
        return user;
    }
    
}

在这里插入图片描述
User user = query().eq(“phone”, phone).one();这段代码使用了mybatisplus,相当于select * from tb_user where phone = ?

为什么要使用HashMap进行存储

在这段代码中,使用HashMap进行存储是为了将用户对象转换成键值对形式,便于统一保存到Redis中,并且可以方便地进行序列化和反序列化操作。具体来说:

  • 便于存储和读取:将用户对象转为HashMap后,可以方便地通过stringRedisTemplate.opsForHash().putAll()方法一次性将整个用户对象存储到Redis的Hash数据结构中,而不需要对用户对象的每个字段分别进行存储。
  • 数据结构清晰:使用HashMap可以清晰地表示用户对象的各个字段和对应的数值,便于管理和维护。
  • 方便序列化和反序列化:HashMap作为Java中的常用数据结构,可以方便地进行序列化(将数据转换为字节序列)反序列化(将字节序列转换为数据)操作,便于在存储到Redis或者从Redis中读取时进行数据格式的转换。

总之,使用HashMap进行存储能够简化代码逻辑,提高数据存储和读取的效率,并且方便进行数据结构的转换和管理。

Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
这段代码为什么要这样写,这些参数有什么用

其中,beanToMap是一个方法,用于将Java对象(Bean)转换为Map类型的数据结构。

在这段代码中,BeanUtil.beanToMap()方法被使用,它是一个工具类方法,可以通过反射机制将Java对象的属性和对应的值转换为键值对形式,并存储到一个Map对象中。

具体来说,beanToMap方法接收三个参数:

  • userDTO:表示要转换的源对象,即需要将其转换为Map的对象。

  • new HashMap<>():表示用于存储转换结果的目标HashMap对象,这里使用了一个新的空HashMap,用于接收转换后的键值对数据。

  • CopyOptions.create().setIgnoreNullValue(true):这是使用BeanUtil进行对象转换时的配置选项。setIgnoreNullValue(true)表示忽略源对象中值为null的属性,不将其放入目标Map中。

  • .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()):这个配置项表示对转换过程中的字段值进行编辑处理。在这里,它的作用是将字段值转换为字符串类型,确保最终存储到Map中的值都是字符串类型。

综合起来,这段代码的目的是将UserDTO对象转换为Map类型,同时忽略空值属性,并确保所有属性值都被转换为字符串类型。这样做的原因可能是为了在存储到Redis中时,确保数据的统一性和一致性,便于后续从Redis中读取并进行处理。

在技术的道路上,我们不断探索、不断前行,不断面对挑战、不断突破自我。科技的发展改变着世界,而我们作为技术人员,也在这个过程中书写着自己的篇章。让我们携手并进,共同努力,开创美好的未来!愿我们在科技的征途上不断奋进,创造出更加美好、更加智能的明天!

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DNSLog漏洞探测(三)之XSS漏洞实战

DNSLog漏洞探测(三)之XSS漏洞实战 通过前面的学习&#xff0c;我们已经明白了什么是DNSLog平台&#xff0c;那么DNSLog平台到底能为我们做些什么呢&#xff1f; DNSLog的平台实际使用很长见的一种情况就是针对漏洞无回显的情况&#xff0c;我们通过让受害者的服务器主动发起对…

Error: Failed to resolve vue/compiler-sfc——vite项目启动报错——npm run serve

运行项目时&#xff0c;报错如下&#xff1a; Error: Failed to resolve vue/compiler-sfc 根据报错信息的提示&#xff1a;vue的版本必须大于3.2.25&#xff0c;经过查看package.json文件&#xff0c;可以看到vue的版本为3.2.36&#xff0c;是满足条件的。 因此考虑缓存问题&…

Python部分基础知识入门学习,十分钟快速上手

文章目录 一、基础语法二、变量类型三、运算符四、条件语句关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 一、…

并发编程的基本概念

进程与线程 进程 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至 CPU&#xff0c;数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的当一个程序被运行&…

【计算机网络】UDP报文详解

目录 一. UDP协议概述 二. UDP报文格式 1. 首部 2. 校验和 三. UDP的缓冲区 结束语 一. UDP协议概述 UDP——用户数据报协议&#xff0c;是传输层的一个重要协议 基于UDP的应用层协议有&#xff1a;DNS&#xff0c;TFTP&#xff0c;SNMP&#xff0c;NTP 协议全称默认端…

pt34-python-Celery

Celery异步网络框架 定义 Celery 是一个简单、灵活且可靠的&#xff0c;处理大量消息的分布式系统&#xff0c;它是一个专注于实时处理的任务队列&#xff0c;同时也支持任务调度。中文官网&#xff1a;http://docs.jinkan.org/docs/celery/ 在线安装 sudo pip3 install -U…

语音验证码有哪些优势

稳定高效 链接各国运营商语音通道&#xff0c;不受时间地域限制&#xff0c;到达率较高&#xff0c;验证效果稳定高效&#xff0c;大大提高了客户回填率和转化率&#xff0c;减少客户流失。 海量并发 智能运维系统&#xff0c;实时监控&#xff0c;自动切换&#xff0c;无需…

【二分查找】【滑动窗口】LeeCode2528:最大化城市的最小电量

作者推荐 【动态规划】【广度优先】LeetCode2258:逃离火灾 本文涉及的基础知识点 二分查找算法合集 滑动窗口 题目 给你一个下标从 0 开始长度为 n 的整数数组 stations &#xff0c;其中 stations[i] 表示第 i 座城市的供电站数目。 每个供电站可以在一定 范围 内给所有城…

PR分屏模板|Premiere动态多画面多屏特效视频模板剪辑素材

这一个很棒的分屏效果PR幻灯片模板视频素材&#xff01;为您的视频制作多屏幕动画&#xff01; 非常易于定制、更改颜色。 仅支持Premier Pro 2024及最新版本。 高清分辨率&#xff1a;19201080/30fps。 持续时间–00:37。 21媒体占位符&#xff08;照片或视频&#xff09;。 包…

Mongdb常用复杂语句(nosql)总结

➡️ ➡️ 关于 MongoDB和MongoTemplate 嵌套数据判空查询 的讨论 ⬅️ ⬅️ 在本篇文章中小名会时常维护些来不及分类的日工作常用的复杂语句&#xff1a; 1、按照表id查询 db.getCollection(TABLE_NAME).find({"_id":ObjectId("62947c8fe2a399286a7259f7&q…

Web网站服务(二)

1、客户机地址限制。 Require all granted&#xff1a;表示允许所有主机访问。 Require all denied&#xff1a;表示拒绝所有主机访问。 Require local&#xff1a;表示仅允许本地主机访问。 Require [not] host <主机名或域名列表>&#xff1a;表示允许或拒绝指定主机或…

2、快速搞定Kafka术语

快速搞定Kafka术语 Kafka 服务端3层消息架构 Kafka 客户端Broker 如何持久化数据小结 Kafka 服务端 3层消息架构 第 1 层是主题层&#xff0c;每个主题可以配置 M 个分区&#xff0c;而每个分区又可以配置 N 个副本。第 2 层是分区层&#xff0c;每个分区的 N 个副本中只能有…

【sqli靶场】第二关和第三关通关思路

目录 前言 一、sqli靶场第二关 1.1 判断注入类型 1.2 判断数据表中的列数 1.3 使用union联合查询 1.4 使用group_concat()函数 1.5 爆出users表中的列名 1.6 爆出users表中的数据 二、sqli靶场第三关 2.1 判断注入类型 2.2 观察报错 2.3 判断数据表中的列数 2.4 使用union联合…

视频目标检测 yolov

目录 yolov相关介绍 预训练是检测动物世界的动物的&#xff0c;1060显卡单帧需要300毫秒 yolov相关介绍 论文地址&#xff1a; https://arxiv.org/pdf/2208.09686.pdf 代码地址&#xff1a; https://github.com/YuHengsss/YOLOV ModelsizemAP50valSpeed 2080Ti(batch size1)…

BGD 实战

梯度下降方法 2.1、三种梯度下降不同 梯度下降分三类&#xff1a;批量梯度下降BGD&#xff08;Batch Gradient Descent&#xff09;、小批量梯度下降MBGD&#xff08;Mini-Batch Gradient Descent&#xff09;、随机梯度下降SGD&#xff08;Stochastic Gradient Descent&…

ROB端口需求

对于一个 4-way 的超标量处理器来说&#xff0c;在重排序缓存&#xff08;ROB&#xff09;中每周期可以退休的指令个数应该是不小于四条的&#xff1b;如图给出的ROB,在那些最旧的指令中,有三条指令都已经变为了complete状态,那么在一个周期内就可以将这三条指令都退休。 要实现…

Vue路由跳转重定向动态路由VueCli

Vue路由跳转&重定向&动态路由&VueCli 一、声明式导航-导航链接 1.需求 实现导航高亮效果 如果使用a标签进行跳转的话&#xff0c;需要给当前跳转的导航加样式&#xff0c;同时要移除上一个a标签的样式&#xff0c;太麻烦&#xff01;&#xff01;&#xff01; …

抖店一件代发怎么做?保姆级运营教程!

我是电商珠珠 抖店是抖音发展的电商平台&#xff0c;一些没有电商经验的新手&#xff0c;不想囤货&#xff0c;担心卖不出去。 听说可以一件代发&#xff0c;但却不知道怎么做。 今天&#xff0c;我就来详细的跟大家讲一下。 一、选品上架 在选品的时候可以参考后台的电商…

GZ029 智能电子产品设计与开发赛题第5套

2023年全国职业院校技能大赛高职组 “GZ029智能电子产品设计与开发”赛项赛卷五 题目&#xff1a;模拟工业传送带物品检测系统的设计与开发 1 竞赛任务 在智能电视机上播放工业传送带传输物品视频&#xff0c;模拟工业传送带物品检测系统&#xff08;以下简称物品检测系统&…

wpf TelerikUI使用DragDropManager

首先&#xff0c;我先创建事务对象ApplicationInfo&#xff0c;当暴露出一对属性当例子集合对于构成ListBoxes。这个类在例子中显示如下代码&#xff1a; public class ApplicationInfo { public Double Price { get; set; } public String IconPath { get; set; } public …