目录

一、短信登录核心原理

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

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 校验验证码、创建用户、生成 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 鉴权” 的模式,解决了分布式系统中登录态共享的问题,同时兼顾了性能和安全性。核心要点:

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

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

更多推荐