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

news2024/11/24 12:08:14

文章目录

  • 🥰前言
  • 🛸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中读取并进行处理。

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

在这里插入图片描述

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

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

相关文章

DNF 单机联网 搭建教程(附视频)

更多游戏搭建&pvf修改教程请见: DNF教程 注意&#xff1a;请不要将游戏进行商业化&#xff0c;一切后果概不负责。仅供单机&#xff0c;好友之间进行娱乐&#xff01;&#xff01; 注意&#xff1a;请不要将游戏进行商业化&#xff0c;一切后果概不负责。仅供单机&#…

重塑未来工作方式,亚马逊云科技re:Invent推出生成式AI助手Amazon Q

亚马逊云科技在re:Invent 2023宣布推出Amazon Q&#xff0c;这是一种新型生成式AI支持的助手&#xff0c;专门用于满足办公场景需要&#xff0c;可以根据客户业务进行定制。客户可以快速获得复杂问题的相关答案、生成内容并采取行动——所有这些都基于客户自身的信息存储库、代…

区块链的可拓展性研究【03】扩容整理

为什么扩容&#xff1a;在layer1上&#xff0c;交易速度慢&#xff0c;燃料价格高 扩容的目的&#xff1a;在保证去中心化和安全性的前提下&#xff0c;提升交易速度&#xff0c;更快确定交易&#xff0c;提升交易吞吐量&#xff08;提升每秒交易量&#xff09; 目前方案有&…

qt 使用百度在线地图 方法2

使用百度在线地图两个关键点&#xff0c;一是html页面准备&#xff1b;二是qt 与js 语言的交互。 1&#xff0c;html页面的准备&#xff0c;双击页面就可以出现如下效果。 主要代码&#xff1a; <!DOCTYPE html> <html> <head><meta http-equiv"C…

亚马逊云科技开发Amazon Bedrock,使构建和移动就像API调用一样简单

企业希望在各种场景中应用生成式AI&#xff0c;例如提高生产效率&#xff0c;创新用户体验和开启全新工作方式。然而&#xff0c;生成式AI技术正快速发展&#xff0c;每天都有新的服务和创新发生。在日新月异的当下&#xff0c;客户的适应能力至关重要。企业需要能够使用最新、…

详解SVG文件

2023年12月12日&#xff0c;周二下午 目录 什么是SVG文件如何查看SVG文件方法1&#xff1a;使用VSCode的"SVG"插件方法2&#xff1a;使用WPS图片​编辑方法3&#xff1a;通过在线网站进行查看怎么制作自己的SVG文件 什么是SVG文件 SVG 是可缩放矢量图形&#xff08…

3_流量预测综述阅读_Cellular traffic prediction with machine learning: A survey

为了方便学习英语书写&#xff0c;总结的一些话用英语书写 ♥目录♥ 0、文献来源and摘要1、introduction2、prediction problems and datasets2.1 prediction problems2.2 dataset&#xff08;1&#xff09;Telecom Italia 意大利电信 2015&#xff08;2&#xff09;City Cell…

深入理解RBAC权限系统

最近&#xff0c;一位朋友在面试中被问及如何设计一个权限系统。我们注意到目前许多后台管理系统&#xff08;包括一些热门的如若依快速开发平台&#xff09;都采用了RBAC访问控制策略。该策略通过将权限授予角色&#xff0c;然后将角色分配给用户&#xff0c;从而实现对系统资…

仿短视频风格的自适应苹果CMS模板源码

这是一款仿短视频风格的自适应苹果CMS模板源码&#xff0c;设计简洁&#xff0c;适合用于搭建个人视频网站或者短视频分享平台。模板支持响应式布局&#xff0c;演示地 址 runruncode.com/yingshimanhau/19650.html 适配各种屏幕尺寸&#xff0c;功能丰富&#xff0c;用户体验良…

在vue3的js中将一组数据赋值的问题

代码: if (res.data) { myPrizeList.value res.data console.log(myPrizeList.value,myPrizeList.value) const giftList ref() console.log(JSON.parse(JSON.stringify(myPrizeList.val…

【VTK】VTK中的宏定义

很高兴在雪易的CSDN遇见你 【vtkWidgetRepresentation】第九期 vtk中的仿射变换 前言 本文分享VTK中的宏定义&#xff0c;希望对各位小伙伴有所帮助&#xff01; 感谢各位小伙伴的点赞关注&#xff0c;小易会继续努力分享&#xff0c;一起进步&#xff01; 你的点赞就是我的…

家用儿童床欧盟CE认证EN716标准

一、标准适用范围 该标准规定了内部长度大于900mm但不超过1400mm的家用童床的安全要求。该安全要求适用于完全组装完毕待用的童床。可以转换成其它产品的童床&#xff08;如&#xff1a;可变产品、游戏床&#xff09;转换后应该符合相关欧洲标准。该标准不适用于提篮、婴儿床和…

Kafka-客户端使用

理解Kafka正确使用方式 Kafka提供了两套客户端API&#xff0c;HighLevel API和LowLevel API。 HighLevel API封装了kafka的运行细节&#xff0c;使用起来比较简单&#xff0c;是企业开发过程中最常用的客户端API。 LowLevel API则需要客户端自己管理Kafka的运行细节&#xf…

快宝技术:连接无代码开发,API集成提升电商营销和用户运营效率

无代码开发&#xff1a;创新的启航 快宝技术自2012年成立至今&#xff0c;一直是无代码开发领域的佼佼者。通过无代码开发平台&#xff0c;快宝技术旨在降低技术门槛&#xff0c;并使非技术人员能够轻松创建和部署应用程序。这不仅使得快递末端软件开发变得高效和便捷&#xf…

用Sketch for Mac轻松创作无限可能的矢量绘图

在如今的数码时代&#xff0c;矢量绘图软件成为了许多设计师和创意爱好者的必备工具。而在众多的矢量绘图软件中&#xff0c;Sketch for Mac无疑是最受欢迎的一款。它以其简洁易用的界面和强大的功能&#xff0c;让用户能够轻松创作出无限可能的矢量图形。 首先&#xff0c;Sk…

Qt Desktop Widgets 控件绘图原理逐步分析拆解

Qt 是目前C语言首选的框架库。之所以称为框架库而不单单是GUI库&#xff0c;是因为Qt提供了远远超过GUI的功能封装&#xff0c;即使不使用GUI的后台服务&#xff0c;也可以用Qt大大提高跨平台的能力。 仅就界面来说&#xff0c;Qt 保持各个平台绘图等效果的统一&#xff0c;并…

QX320F28346,TI的TMS320F28346定制的DSP吗?为什么没有模拟外设ADC、DAC等?

QX320F28346&#xff0c;TI的TMS320F28346定制的DSP吗&#xff1f;为什么没有模拟外设ADC、DAC等&#xff1f;

基于Java+vue的音乐网站设计与实现(源码+文档+数据库)

摘 要 在此基础上&#xff0c;提出了一种基于javavue的在线音乐排行榜系统的设计与实现方法。本系统分为两个大的功能&#xff0c;即&#xff1a;前端显示、后端管理。而在前台&#xff0c;则是播放不同的歌曲&#xff0c;让人可以在上面观看不同的歌曲&#xff0c;也可以观看…

Vue--第八天

Vue3 1.优点&#xff1a; 2.创建&#xff1a; 3.文件&#xff1a; 换运行插件&#xff1a; 4.运行&#xff1a; setup函数&#xff1a; setup函数中获取不到this&#xff08;this 在定义的时候是Undefined) reactive()和ref(): 代码&#xff1a; <script setup> // …

springboot listener、filter登录实战

转载自&#xff1a; www.javaman.cn 博客系统访问&#xff1a; http://175.24.198.63:9090/front/index 登录功能 1、前端页面 采用的是layui-admin框架&#xff0c;文中的验证码内容&#xff0c;请参考作者之前的验证码功能 <!DOCTYPE html> <html lang"zh…