基于 Redis 实现短信登录:安全高效的登录方案详解
目录
3.1.2 配置 Redis(application.yml
2.1 控制器代码 文件位置:UserController.java
第三阶段:用户登录(验证 → 查询 → 生成token → 存储)
3.1 Service层登录核心代码 文件位置 : UserServiceImpl.java
前引:在分布式系统中,传统的 Session 登录方案存在跨服务共享困难、服务器内存占用高的问题。而 Redis 凭借其高性能、分布式友好、支持键过期的特性,成为实现短信登录的理想选择。本文将从原理、流程到代码实现,全方位讲解如何基于 Redis 构建短信登录功能。
一、短信登录核心原理
短信登录的核心逻辑是:用手机号作为身份标识,通过验证码完成身份验证,验证通过后生成临时令牌(Token),将用户信息缓存到 Redis 中,后续请求通过 Token 从 Redis 获取用户信息完成鉴权。
相比传统 Session 登录,Redis 方案的优势:
分布式友好:Redis 作为中心化缓存,多服务实例可共享用户登录状态;
可控的过期策略:验证码、Token 均可设置精准的过期时间,提升安全性;
高性能:Redis 基于内存操作,读写速度远高于数据库。
二、短信登录核心流程(附流程图)
2.1 整体流程


2.2 关键节点说明
验证码过期时间:设置 5 分钟(300 秒),防止验证码被长期滥用;
Token 设计:采用 UUID 作为唯一标识,Redis 中 Token 的过期时间设为 2 小时,兼顾安全性和用户体验;
用户信息存储:将用户信息序列化为 JSON 存入 Redis,避免频繁查询数据库。
三、代码实现(Spring Boot + Redis)
第一阶段: 环境准备
3.1.1 引入 Maven 依赖
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 工具类:生成UUID、JSON序列化 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
3.1.2 配置 Redis(application.yml
spring:
# Redis配置
redis:
host: localhost # Redis地址
port: 6379 # 端口
password: # 无密码则留空
database: 0 # 使用第0个库
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: 1000ms # 连接等待时间
# 服务端口
server:
port: 8080
第二阶段:发送验证码(接口 → 存储)
2.1 控制器代码 文件位置:UserController.java
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 直接调用Service层处理业务逻辑
return userService.sendCode(phone, session);
}
/**
* 登录功能
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
// 直接调用Service层处理业务逻辑
return userService.login(loginForm, session);
}
// 其他接口...
}
2.2 Service层核心代码
文件位置 : UserServiceImpl.java
@Override
public Result sendCode(String phone, HttpSession session) {
// ========== 第1步:校验手机号格式 ==========
// 调用正则工具类校验手机号是否符合格式
if(RegexUtils.isPhoneInvalid(phone)) {
// 格式错误,返回错误信息
return Result.fail("手机号格式错误");
}
// ========== 第2步:生成验证码 ==========
// 使用 Hutool 工具类生成 6 位随机数字验证码
String code = RandomUtil.randomNumbers(6);
// ========== 第3步:存储验证码到 Redis ==========
// 键名格式:login:code:手机号
// 使用 opsForValue() 操作 String 类型
stringRedisTemplate.opsForValue().set(
LOGIN_CODE_KEY + phone, // key
code, // value
LOGIN_CODE_TTL, // 2
TimeUnit.MINUTES // 时间单位:分钟
);
// ========== 第4步:模拟发送短信 ==========
// 生产环境需要集成短信服务商(如阿里云、腾讯云)
log.debug("发送短信验证码成功,验证码:{}", code);
// ========== 第5步:返回成功结果 ==========
return Result.ok();
}
第三阶段:用户登录(验证 → 查询 → 生成token → 存储)
3.1 Service层登录核心代码 文件位置 : UserServiceImpl.java
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// ========== 第1步:校验手机号 ==========
String phone = loginForm.getPhone(); // 获取手机号
if(RegexUtils.isPhoneInvalid(phone)) { // 校验格式
return Result.fail("手机号格式错误");
}
// ========== 第2步:从 Redis 获取验证码 ==========
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode(); // 获取用户输入的验证码
// ========== 第3步:校验验证码 ==========
// Redis 中无验证码 或 验证码不匹配
if(cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("验证码错误");
}
// ========== 第4步:根据手机号查询用户 ==========
// 使用 MyBatis Plus 的 query() 方法链式查询
User user = query().eq("phone", phone).one();
// ========== 第5步:判断用户是否存在,不存在则创建 ==========
if(user == null) {
// 调用方法创建新用户
user = createUserWithPhone(phone);
}
// ========== 第6步:生成登录令牌(token) ==========
// 使用 UUID 生成唯一的登录凭证
// true 表示不带连字符,更短更美观
String token = UUID.randomUUID().toString(true);
// ========== 第7步:处理用户信息 ==========
// 7.1 将 User 实体转换为 UserDTO(脱敏处理,不返回敏感信息)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 7.2 将 DTO 转换为 Map(用于 Redis Hash 存储)
// 使用 Hutool 的 BeanUtil 进行转换
Map<String, Object> userMap = BeanUtil.beanToMap(
userDTO, // 源对象
new HashMap<>(), // 目标 Map
CopyOptions.create() // 拷贝选项
.setIgnoreNullValue(true) // 忽略空值字段
.setFieldValueEditor((fieldName, fieldValue) ->
fieldValue.toString()) // 所有字段值都转为 String 类型
);
// ========== 第8步:存储用户信息到 Redis ==========
// 键名格式:login:token:uuid
String tokenKey = LOGIN_USER_KEY + token;
// 使用 Hash 结构存储用户信息,相比 String 更节省内存
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 设置 token 过期时间:30分钟
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// ========== 第9步:返回 token 给前端 ==========
// 前端会将 token 存储在 sessionStorage 中,后续请求携带在请求头
return Result.ok(token);
}
/**
* 创建新用户
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
// 使用系统常量作为前缀,生成随机昵称
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
save(user); // 保存到数据库
return user;
}
第四阶段:登录拦截器(验证 → 身份识别)
4.1 Token刷新拦截器
文件位置 : RrfreshTokenInterceptor.java
package com.hmdp.utils;
public class RrfreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
// 构造函数注入
public RrfreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ========== 第1步:从请求头获取 token ==========
String token = request.getHeader("authorization");
// ========== 第2步:判断 token 是否为空 ==========
if(StrUtil.isBlank(token)) {
// token 为空,直接放行,交给 loginInterceptor 处理
return true;
}
// ========== 第3步:根据 token 从 Redis 获取用户信息 ==========
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(LOGIN_USER_KEY + token);
// ========== 第4步:判断用户信息是否存在 ==========
if(userMap.isEmpty()) {
// 用户信息不存在,直接放行
return true;
}
// ========== 第5步:将 Map 转换为 UserDTO ==========
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// ========== 第6步:存入 ThreadLocal ==========
UserHolder.saveUser(userDTO);
// ========== 第7步:刷新 token 过期时间 ==========
// 用户活跃则自动续期,延长登录状态
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
// ========== 第8步:放行 ==========
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// ========== 第9步:清理 ThreadLocal,防止内存泄漏 ==========
UserHolder.removeUser();
}
}
4.2 登录拦截器
文件位置 : loginInterceptor.java
package com.hmdp.utils;
public class loginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public loginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ========== 第1步:检查 ThreadLocal 中是否有用户 ==========
// 由于 RrfreshTokenInterceptor 已经先执行,如果有用户会存入 ThreadLocal
if(UserHolder.getUser() != null) {
// 有用户,放行
return true;
}
// ========== 第2步:没有用户,拦截请求,返回 401 ==========
response.setStatus(401);
return false;
}
}
4.3拦截器配置
文件位置 : MvcConfig.java
package com.hmdp.config;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// ========== 第1步:配置登录拦截器 ==========
registry.addInterceptor(new loginInterceptor(stringRedisTemplate))
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns( // 排除以下路径
"/user/code", // 发送验证码
"/user/login", // 用户登录
"/blog/hot", // 热门博客
"/shop/**", // 商铺相关
"/shop-type/**", // 商铺类型
"/upload/**" // 文件上传
)
.order(1); // 优先级:数字越大越晚执行
// ========== 第2步:配置 Token 刷新拦截器 ==========
registry.addInterceptor(new RrfreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**") // 拦截所有请求
.order(0); // 优先级:数字越小越先执行
}
}
4.4 用户信息存储工具
文件位置 : UserHolder.java
package com.hmdp.utils;
public class UserHolder {
// 使用 ThreadLocal 存储用户信息,每个线程独立存储,互不干扰
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();
}
}
四:功能设计
4.1 Redis 操作核心
使用StringRedisTemplate操作 Redis,优势是直接操作字符串,避免序列化问题;所有 Key 都添加前缀(如SMS_CODE:),方便 Redis 管理和排查。
4.2 验证码安全设计
验证码生成:采用随机 6 位数字,避免规律;
验证码过期:5 分钟自动失效,防止暴力破解;
验证码删除:登录成功后立即删除 Redis 中的验证码,防止重复使用。
4.3 Token 设计与优化
Token 生成:UUID 保证唯一性,避免被猜测;
Token 过期:2 小时有效期,兼顾安全性和用户体验;
Token 刷新:拦截器中刷新过期时间,用户活跃时自动延长登录态。
4.4 ThreadLocal 的使用
将用户信息存入ThreadLocal,避免在接口中反复从 Redis 获取,同时保证多线程环境下用户信息隔离,请求结束后清空ThreadLocal防止内存泄漏。
五、功能表
| 模块文件 | 核心功能 | 依赖的其他模块 / 组件 | 与 Redis 的核心交互 | 核心作用 |
|---|---|---|---|---|
| UserController.java | 接收验证码 / 登录请求、返回用户信息 | StringRedisTemplate、UserService | 存储短信验证码(String) | 请求入口,承接前端与业务层 |
| UserServiceImpl.java | 校验验证码、创建用户、生成 Token | StringRedisTemplate、UserMapper | 校验验证码、存储登录态(Hash)、删除验证码 | 业务核心,完成登录全逻辑 |
| RefreshTokenInterceptor.java | Token 续期、存储用户信息到 ThreadLocal | StringRedisTemplate、UserHolder | 读取登录态、刷新 Token 过期时间 | 维持登录态,减少 Redis 查询 |
| LoginInterceptor.java | 校验用户是否登录 | UserHolder | 无(依赖 RefreshTokenInterceptor 结果) | 拦截未登录请求,返回 401 |
| MvcConfig.java | 注册拦截器、指定执行顺序 | RefreshTokenInterceptor、LoginInterceptor | 无 | 管控拦截器执行逻辑,放行免鉴权接口 |
| UserHolder.java | 存储 / 获取 / 移除线程内用户信息 | 无(被拦截器 / Controller 依赖) | 无 | 线程内共享用户信息,解耦数据传递 |
redis
| 数据类型 | Key 格式 | Value/Field 说明 | 过期时间 | 作用场景 |
|---|---|---|---|---|
| String | login:code:{手机号} | 6 位数字验证码 | 5 分钟 | 短信验证码存储 / 校验 |
| Hash | login:token:{UUID} | id: 用户 ID、phone: 手机号、nickName: 昵称 | 30 分钟(可续期) | 存储用户登录态、鉴权 |
六、总结
基于 Redis 的短信登录方案,通过 “验证码验证 + Token 鉴权” 的模式,解决了分布式系统中登录态共享的问题,同时兼顾了性能和安全性。核心要点:
- 合理设计 Redis 的 Key 和过期时间;
- 验证码和 Token 的安全管控(过期、防重复使用);
- 利用 ThreadLocal 简化用户信息传递;
- 拦截器统一处理登录态验证,降低业务代码耦合。
更多推荐




所有评论(0)