使用缓存保存验证码进行登录校验

news2025/2/23 4:56:08

在Spring Boot项目中使用Redis进行登录校验,一般的做法是将用户的登录状态(例如,JWT令牌或者用户信息)存储在Redis中,并在后续请求中进行校验。

我们需要建立两个拦截器:RefreshTokenInterceptor + LoginInterceptor,它们分别拦截全部路径和拦截需要登录的路径:

首先我们需要将这个token从前端进行获取,随后在Redis缓存中查询是否有该用户的token,如果没有就会返回401,如果查到该用户的token则进行获取数据,将用户信息保存在ThreadLocal线程空间内。

为什么要设置两个拦截器?

因为有的部分功能模块是不需要进行拦截处理,所以我们将所有模块都需要处理的步骤提取并加入新的拦截器,随后用登录校验的拦截器进行判断是否拦截,只有线程空间拥有值的情况,才能证明是有效用户,因为在全部拦截的拦截器,只有满足:

1.前端有token返回       2.redis内有用户信息

才能在线程空间存储数据。

首先创建拦截一切的 RefreshTokenInterceptor 拦截器:

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.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

//    // 手动注入:如果不加入@Component,那么需要在MvcConfig进行注入StringRedisTemplate,然后在new RefreshTokenInterceptor()的括号内加入stringRedisTemplate
//    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
//        this.stringRedisTemplate = stringRedisTemplate;
//    }

//    @Override
//    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        // 获取session
//        HttpSession session = request.getSession();
//        // 获取sesstion用户
//        Object user = session.getAttribute("user");
//        if (user == null) {
//            response.setStatus(401);
//            return false;
//        }
//        UserHolder.saveUser((UserDTO)user);
//        return true;
//    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("authorization");
        // 判断token是否为空
        if(StrUtil.isBlank(token)){
            return true;
        }
        // 通过token获取redis缓存中的用户信息(判断token是否有效)
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
        // 判断用户是否存在
        if(userMap.isEmpty()){
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
        UserHolder.saveUser(userDTO);
        // 刷新有效时间(为了不让用户正在使用但是出现token过期的情况,即使刷新token有效期)
        stringRedisTemplate.expire("login:token:" + token,30, TimeUnit.MINUTES);
        return true;
    }

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

}

从上面代码中我们可以看出,在该拦截器是不会直接拦截处理,而是全部放行。

那么线程空间如何创建的呢?

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

那我们怎么知道的token是合法的?

因为在UserController中,在登录成功后我们将token作为key,将用户信息作为value,使用hash进行存储数据,这样通过判断缓存中是否有token就可以判断该用户是否合法:

// 生成token,将user内的属性拷贝到userDTO以此存入redis
String token = UUID.randomUUID().toString();
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 将对象变成map进行存储,因为Redis的类型都需要是String,所以需要将内部的值转换成String类型
Map<String,Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
       CopyOptions.create().setIgnoreNullValue(true)
              .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll("login:token:" + token,map);
// 设置token有效期
stringRedisTemplate.expire("login:token:" + token,30, TimeUnit.MINUTES);

随后创建登录拦截器 LoginInterceptor :

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

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

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

只有token有值且合法的情况才会通过此拦截器,所以这样就能够通过两个拦截器来有效处理用户正在使用但token过期后跳转问题。

既然我们已经创建成功两个拦截器,接下来就需要注册拦截器,在 /config/MvcConfig 文件中创建:

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/login","/user/code",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**"
                ).order(1); // 值越大的越后执行
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor()).addPathPatterns("/**").order(0);// 拦截所有请求
    }
}

通过配置权值来决定拦截的先后顺序,在order()内进行写入,权值越大越后执行。

随后我们就要解决短信验证码这个问题了 ->

在UserController中创建两个方法分别对验证码以及登录校验进行处理

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone,session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm,session);
    }
}

在以前,我们对于token存储问题都会使用session,而现在缓存是有利于存储且获取的一种手段,我们可以将token存储至缓存,随后在拦截器中通过查找是否有 key = token 的数据来判断token是否有效 ->

首先将验证码生成并保存在缓冲中,使用phone作为key来记录,以此保证唯一:

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

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式错误");
        }
        // 生成验证码
        String code = RandomUtil.randomNumbers(6);

//        // 保存验证码到sesson
//        session.setAttribute("code", code);
        // 保存验证码到Redis
        stringRedisTemplate.opsForValue().set("login:code:" + phone,code,2, TimeUnit.MINUTES);

        // 发送验证码(模拟步骤)
        log.debug("发送短信验证码成功,验证码:{}",code);

        return Result.ok();
    }
}

这里设置有效期为两分钟,接下来我们的登录校验方法就可以通过在缓存中获取验证码来进行校验:

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

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式错误");
        }
//        Object cacheCode = session.getAttribute("code");
        // 从Redis获取验证码来进行校验
        String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.equals(code)){
            return Result.fail("验证码错误");
        }
        // 一致,则根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        if(user == null){
            user = createUserWithPhone(phone);
        }
//        // 将user内的属性拷贝到userDTO以此存入session
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        // 生成token,将user内的属性拷贝到userDTO以此存入redis
        String token = UUID.randomUUID().toString();
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // 将对象变成map进行存储,因为Redis的类型都需要是String,所以需要将内部的值转换成String类型
        Map<String,Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll("login:token:" + token,map);
        // 设置token有效期
        stringRedisTemplate.expire("login:token:" + token,30, TimeUnit.MINUTES);
        return Result.ok(token);
    }

    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName("user_" + RandomUtil.randomString(10)); // 随机昵称
        // 保存用户
        save(user);
        return user;
    }
}

这里校验手机号的格式使用的工具类:

public abstract class RegexPatterns {
    /**
     * 手机号正则
     */
    public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
    /**
     * 邮箱正则
     */
    public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
    /**
     * 密码正则。4~32位的字母、数字、下划线
     */
    public static final String PASSWORD_REGEX = "^\\w{4,32}$";
    /**
     * 验证码正则, 6位数字或字母
     */
    public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";

}
public class RegexUtils {
    /**
     * 是否是无效手机格式
     * @param phone 要校验的手机号
     * @return true:符合,false:不符合
     */
    public static boolean isPhoneInvalid(String phone){
        return mismatch(phone, RegexPatterns.PHONE_REGEX);
    }
    /**
     * 是否是无效邮箱格式
     * @param email 要校验的邮箱
     * @return true:符合,false:不符合
     */
    public static boolean isEmailInvalid(String email){
        return mismatch(email, RegexPatterns.EMAIL_REGEX);
    }

    /**
     * 是否是无效验证码格式
     * @param code 要校验的验证码
     * @return true:符合,false:不符合
     */
    public static boolean isCodeInvalid(String code){
        return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
    }

    // 校验是否不符合正则格式
    private static boolean mismatch(String str, String regex){
        if (StrUtil.isBlank(str)) {
            return true;
        }
        return !str.matches(regex);
    }
}

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

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

相关文章

会议签到系统的架构和实现

会议签到系统的架构和实现 摘要:通过定制安卓会议机开机APP呈现签到界面&#xff0c;并且通过W/B结构采集管理签到信息&#xff0c;实现会议签到的功能。为达到此目标本文将探讨使用Redis提供后台数据支持&#xff1b;使用SocketIo处理适时消息&#xff1b;使用Flask进行原型开…

通过Ukey或者OTP动态口令实现windows安全登录

通过 安当SLA&#xff08;System Login Agent&#xff09;实现Windows安全登录认证&#xff0c;是一种基于双因素认证&#xff08;2FA&#xff09;的解决方案&#xff0c;旨在提升 Windows 系统的登录安全性。以下是详细的实现方法和步骤&#xff1a; 1. 安当SLA的核心功能 安…

.Net Core微服务入门系列(一)——项目搭建

系列文章目录 1、.Net Core微服务入门系列&#xff08;一&#xff09;——项目搭建 2、.Net Core微服务入门全纪录&#xff08;二&#xff09;——Consul-服务注册与发现&#xff08;上&#xff09; 3、.Net Core微服务入门全纪录&#xff08;三&#xff09;——Consul-服务注…

Ubuntu 24.04 LTS 安装 Docker Desktop

Docker 简介 Docker 简介和安装Ubuntu上学习使用Docker的详细入门教程Docker 快速入门Ubuntu版&#xff08;1h速通&#xff09; Docker 安装 参考 How to Install Docker on Ubuntu 24.04: Step-by-Step Guide。 更新系统和安装依赖 在终端中运行以下命令以确保系统更新并…

Spring MVC:设置响应

目录 引言 1. 返回静态页面 1.1 Spring 默认扫描路径 1.2 RestController 1.2.1 Controller > 返回页面 1.2.2 ResponseBody 2. 返回 HTML 2.1 RequestMapping 2.1.1 produces(修改响应的 Content-Type) 2.1.2 其他属性 3. 返回 JSON 4. 设置状态码 4.1 HttpSer…

GD32F303 GCC 环境搭建

一、引言 在嵌入式开发领域&#xff0c;GD32F303 微控制器以其出色的性能和丰富的功能被广泛应用。为了充分发挥其潜力&#xff0c;搭建一个高效的开发环境并深入理解项目构建过程至关重要。本文将详细介绍如何基于 GCC 工具链搭建 GD32F303 的开发环境&#xff0c;重点聚焦于…

路径规划之启发式算法之二十八:候鸟优化算法(Migrating Birds Optimization, MBO)

候鸟优化算法(Migrating Birds Optimization, MBO)是一种基于群体智能的元启发式优化算法,其灵感来源于候鸟迁徙时的“V”字形飞行队列。这种队列结构能够有效减少能量消耗,同时提高飞行效率。MBO算法通过模拟候鸟的迁徙行为,利用群体间的协作和信息共享来优化问题的解。 …

ESP8266 MQTT服务器+阿里云

MQTT私有平台搭建&#xff08;EMQX 阿里云&#xff09; 阿里云服务器 EMQX 搭建私有MQTT平台 1、搜索EMQX开源版本 2、查看各版本EMQX支持的UBUNTU版本 3、查看服务器Ubuntu版本 4、使用APT安装模式 5、按照官网指示安装并启动 6、下载安装MQTTX测试工具 7、设置云服务…

【机器学习实战中阶】使用SARIMAX,ARIMA预测比特币价格,时间序列预测

数据集说明 比特币价格预测&#xff08;轻量级CSV&#xff09;关于数据集 致谢 这些数据来自CoinMarketCap&#xff0c;并且可以免费使用该数据。 https://coinmarketcap.com/ 数据集:链接: 价格预测器 源代码与数据集 算法说明 SARIMAX&#xff08;Seasonal AutoRegressive …

Postgresql源码(140)理解PG的编译流程(make、Makefile、Makefile.global.in)

PG16 PG中使用的makefile看起来代码比较多&#xff0c;但是实际逻辑比较简单&#xff0c;这里做一些抽象总结。 总结 Makefile.global.in的$(recurse)宏自动生成了target&#xff0c;可以方便的进入内存目录进行编译。 all: all-common-recurse all-common-recurse: submak…

Java数据结构——优先队列

目录 引言1. 优先队列2. 优先队列的实现2.1 堆的概念2.2 堆的创建2.2.1 堆的向下调整2.3 堆的插入2.4 堆的删除 3. 总结 引言 前面一篇文章讲了二叉树&#xff0c;本篇将讲述数据结构中的优先队列&#xff0c;优先队列则需要用到完全二叉树来实现。 1. 优先队列 队列&#x…

51c大模型~合集105

我自己的原文哦~ https://blog.51cto.com/whaosoft/13101924 #刚刚&#xff0c;ChatGPT开始有了执行力&#xff01; 现在 AI 智能体可以 24*7 小时为你打工。 2025 刚过去了半个月&#xff0c;OpenAI 在智能体领域「开大」了。 今天&#xff0c;OpenAI 正在为 ChatGPT 推出…

从零到上线:Node.js 项目的完整部署流程(包含 Docker 和 CICD)

从零到上线&#xff1a;Node.js 项目的完整部署流程&#xff08;包含 Docker 和 CI/CD&#xff09; 目录 项目初始化&#xff1a;构建一个简单的 Node.js 应用设置 Docker 环境&#xff1a;容器化你的应用配置 CI/CD&#xff1a;自动化构建与部署上线前的最后检查&#xff1a;…

《自动驾驶与机器人中的SLAM技术》ch4:基于预积分和图优化的 GINS

前言&#xff1a;预积分图优化的结构 1 预积分的图优化顶点 这里使用 《自动驾驶与机器人中的SLAM技术》ch4&#xff1a;预积分学 中提到的散装的形式来实现预积分的顶点部分&#xff0c;所以每个状态被分为位姿&#xff08;&#xff09;、速度、陀螺零偏、加计零偏四种顶点&am…

Ubuntu系统更改IP,保姆级教程

原理概述 本篇文章所用工具&#xff1a; Xshell&#xff1a;点击下载 VMware Workstation Pro&#xff1a;点击下载 密钥需要自行搜索所下载的VMware对应版本密钥。 IP 地址 IP 地址&#xff08;Internet Protocol Address&#xff09;是分配给每个连接到计算机网络的设备的…

【Linux】进程管理(一篇入门-进程:基本概念、PCB进程块、进程的创建、等待、终止、状态,exec函数族的使用)

本节主要内容&#xff1a;进程的一些基本概念&#xff0c;进程控制块PCB&#xff0c;以及如何在一个C程序里创建进程、终止进程、等待进程&#xff0c;包括孤儿进程、僵尸进程等特殊状态的进程&#xff0c;并涉及回收进程的概念。最终是exec函数族的函数使用方法及用途。 程序与…

RabbitMQ 在实际应用时要注意的问题

1. 幂等性保障 1.1 幂等性介绍 幂等性是数学和计算机科学中某些运算的性质,它们可以被多次应⽤,⽽不会改变初始应⽤的结果. 应⽤程序的幂等性介绍 在应⽤程序中,幂等性就是指对⼀个系统进⾏重复调⽤(相同参数),不论请求多少次,这些请求对系统的影响都是相同的效果. ⽐如数据库…

EXCEL+Python搞定数据处理(第一部分:Python入门-第1章:为什么要用Python为Excel编程)

参考资料&#xff1a; ExcelPython飞速搞定数据分析与处理&#xff0c;[瑞士] 费利克斯朱姆斯坦 著&#xff0c;中国工信出版社、人民邮电出版社出版(“Python for Excel, by Felix Zumstein (O’Reilly). Copyright 2021 Zoomer Analytics LLC, 978-1-492-08100-5”) 将不定…

MPLS VPN 部署与应用

一.简介 MPLS&#xff0c;称之为多协议标签交换&#xff0c;在九十年代中期被提出来&#xff0c;用于解决传统IP报文依赖查表转发而产生的瓶颈&#xff0c;现多用于VPN技术&#xff0c;MPLS报头封装在数据链路层之上&#xff0c;网络层之下。本文为结合了华为技术和新华三技术…

麒麟操作系统服务架构保姆级教程(十三)tomcat环境安装以及LNMT架构

如果你想拥有你从未拥有过的东西&#xff0c;那么你必须去做你从未做过的事情 之前咱们学习了LNMP架构&#xff0c;但是PHP对于技术来说确实是老掉牙了&#xff0c;PHP的市场占有量越来越少了&#xff0c;我认识一个10年的PHP开发工程师&#xff0c;十年工资从15k到今天的6k&am…