目录

一、短信登录核心原理

二、短信登录核心流程(附流程图)

2.1 整体流程

2.2 关键节点说明

三、代码实现(Spring Boot + Redis)

第一阶段: 环境准备

3.1.1 引入 Maven 依赖

3.1.2 配置 Redis(application.yml

第二阶段:发送验证码(接口 → 存储)

2.1 控制器代码   文件位置:UserController.java

2.2 Service层核心代码 

第三阶段:用户登录(验证 → 查询 → 生成token → 存储)

 3.1 Service层登录核心代码   文件位置 : UserServiceImpl.java

第四阶段:登录拦截器(验证 → 身份识别)

4.1 Token刷新拦截器

4.2 登录拦截器

4.3拦截器配置   

4.4 用户信息存储工具

四:功能设计

4.1 Redis 操作核心

4.2 验证码安全设计

4.3 Token 设计与优化

4.4 ThreadLocal 的使用

五、功能表

六、总结

前引:在分布式系统中,传统的 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校验验证码、创建用户、生成 TokenStringRedisTemplate、UserMapper校验验证码、存储登录态(Hash)、删除验证码业务核心,完成登录全逻辑
RefreshTokenInterceptor.javaToken 续期、存储用户信息到 ThreadLocalStringRedisTemplate、UserHolder读取登录态、刷新 Token 过期时间维持登录态,减少 Redis 查询
LoginInterceptor.java校验用户是否登录UserHolder无(依赖 RefreshTokenInterceptor 结果)拦截未登录请求,返回 401
MvcConfig.java注册拦截器、指定执行顺序RefreshTokenInterceptor、LoginInterceptor无管控拦截器执行逻辑,放行免鉴权接口
UserHolder.java存储 / 获取 / 移除线程内用户信息无(被拦截器 / Controller 依赖)无线程内共享用户信息,解耦数据传递

redis

数据类型Key 格式Value/Field 说明过期时间作用场景
Stringlogin:code:{手机号}6 位数字验证码5 分钟短信验证码存储 / 校验
Hashlogin:token:{UUID}id: 用户 ID、phone: 手机号、nickName: 昵称30 分钟(可续期)存储用户登录态、鉴权

六、总结

基于 Redis 的短信登录方案,通过 “验证码验证 + Token 鉴权” 的模式,解决了分布式系统中登录态共享的问题,同时兼顾了性能和安全性。核心要点:

  1. 合理设计 Redis 的 Key 和过期时间;
  2. 验证码和 Token 的安全管控(过期、防重复使用);
  3. 利用 ThreadLocal 简化用户信息传递;
  4. 拦截器统一处理登录态验证,降低业务代码耦合。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐