基于Redis实现短信登录

一、前言:为什么越来越多系统采用"短信登录"?

随着用户体验要求提升,"手机号 + 短信验证码"登录方式已成主流:

  • ✅ 无需记忆密码,降低用户流失
  • ✅ 天然绑定手机号,便于后续触达
  • ✅ 符合国内实名制趋势

但实现一个安全、稳定、防刷的短信登录功能,并非简单"发个验证码"即可。

本文将带你用 Spring Boot + Redis 实现一套生产可用的短信登录系统 ,涵盖:

✅ 验证码生成与存储

✅ 双重限流(IP + 手机号)

✅ 登录态管理(Token + Redis)

✅ 安全校验与防重放

✅ 完整代码示例


二、整体业务流程

复制代码
1. 用户输入手机号,点击【获取验证码】
        ↓
2. 后端校验图形验证码(防机器刷)
        ↓
3. 检查 IP 和手机号发送频率(防刷)
        ↓
4. 生成 6 位随机码,存入 Redis(key = sms:code:{phone})
        ↓
5. 调用短信平台发送(本文模拟)
        ↓
6. 用户输入验证码,点击【登录】
        ↓
7. 校验验证码是否正确 & 未过期 & 未使用
        ↓
8. 验证通过 → 生成登录 Token,存入 Redis(key = login:token:{uuid})
        ↓
9. 返回 Token 给前端,后续请求携带 Token 认证

🔑 核心思想Redis 既是验证码仓库,也是会话管理中心


三、技术选型

组件 作用
Spring Boot 3.x Web 框架
Redis 存储验证码 + 登录态
StringRedisTemplate 操作 Redis(字符串友好)
EasyCaptcha 生成图形验证码(防自动化脚本)
UUID 生成唯一登录 Token

四、核心实现步骤

1. 添加依赖

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

2. Redis Key 命名规范

java 复制代码
// 验证码
private static final String SMS_CODE_KEY = "sms:code:%s"; // %s = phone

// 验证码发送频率(60秒内限1次)
private static final String SMS_LIMIT_KEY = "sms:limit:%s";

// 登录 Token
private static final String LOGIN_TOKEN_KEY = "login:token:%s"; // %s = token

// 用户信息(可选)
private static final String USER_INFO_KEY = "user:info:%s"; // %s = phone

3. 图形验证码接口(防刷前置)

java 复制代码
@RestController
public class CaptchaController {

    @GetMapping("/captcha")
    public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48);
        captcha.setLen(4);
        request.getSession().setAttribute("captcha", captcha.text());
        response.setContentType("image/png");
        captcha.out(response.getOutputStream());
    }
}

4. 发送短信验证码接口

java 复制代码
@RestController
public class SmsLoginController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final long CODE_EXPIRE = 300; // 5分钟
    private static final long PHONE_LIMIT = 60;  // 60秒内限1次
    private static final int IP_DAILY_LIMIT = 20; // IP 日限额

    @PostMapping("/send-sms-code")
    public ResponseEntity<String> sendSmsCode(
            @RequestParam String phone,
            @RequestParam String captchaInput,
            HttpServletRequest request) {

        // 1. 校验图形验证码
        String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
        if (!captchaInput.equals(sessionCaptcha)) {
            return ResponseEntity.badRequest().body("图形验证码错误");
        }

        // 2. 校验手机号格式
        if (!phone.matches("^1[3-9]\\d{9} $ ")) {
            return ResponseEntity.badRequest().body("手机号格式错误");
        }

        String ip = getClientIP(request);

        // 3. IP 日限额检查
        String ipLimitKey = "sms:ip:" + ip;
        Integer ipCount = getRedisInt(ipLimitKey, 24 * 3600);
        if (ipCount >= IP_DAILY_LIMIT) {
            return ResponseEntity.status(429).body("操作过于频繁");
        }

        // 4. 手机号频率检查(60秒内只能发1次)
        String phoneLimitKey = String.format(SMS_LIMIT_KEY, phone);
        Boolean hasSent = redisTemplate.hasKey(phoneLimitKey);
        if (Boolean.TRUE.equals(hasSent)) {
            return ResponseEntity.status(429).body("请60秒后再试");
        }

        // 5. 生成验证码
        String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));

        // 6. 存入 Redis
        redisTemplate.opsForValue().set(
            String.format(SMS_CODE_KEY, phone), 
            code, 
            CODE_EXPIRE, 
            TimeUnit.SECONDS
        );
        redisTemplate.opsForValue().set(phoneLimitKey, "1", PHONE_LIMIT, TimeUnit.SECONDS);
        redisTemplate.opsForValue().increment(ipLimitKey);
        redisTemplate.expire(ipLimitKey, 24, TimeUnit.HOURS);

        // 7. 【模拟】发送短信
        System.out.println("【测试】向 " + phone + " 发送验证码: " + code);

        return ResponseEntity.ok("验证码已发送");
    }

    private Integer getRedisInt(String key, int expireSeconds) {
        String val = redisTemplate.opsForValue().get(key);
        if (val == null) {
            redisTemplate.opsForValue().set(key, "0", expireSeconds, TimeUnit.SECONDS);
            return 0;
        }
        return Integer.parseInt(val);
    }

    private String getClientIP(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

5. 短信登录接口(核心!)

java 复制代码
@PostMapping("/sms-login")
public ResponseEntity<Map<String, Object>> smsLogin(@RequestParam String phone,
                                                     @RequestParam String code) {
    // 1. 校验验证码
    String realCode = redisTemplate.opsForValue().get(String.format(SMS_CODE_KEY, phone));
    if (realCode == null) {
        return ResponseEntity.badRequest().body(Map.of("msg", "验证码已过期"));
    }
    if (!realCode.equals(code)) {
        return ResponseEntity.badRequest().body(Map.of("msg", "验证码错误"));
    }

    // 2. 【关键】验证成功后立即删除验证码(防重放)
    redisTemplate.delete(String.format(SMS_CODE_KEY, phone));

    // 3. 生成登录 Token
    String token = UUID.randomUUID().toString().replace("-", "");

    // 4. 构造用户信息(实际应查数据库)
    String userInfo = "{\"phone\":\"" + phone + "\",\"userId\":1001}";

    // 5. 存入 Redis(2小时有效)
    redisTemplate.opsForValue().set(
        String.format(LOGIN_TOKEN_KEY, token),
        userInfo,
        2 * 3600,
        TimeUnit.SECONDS
    );

    // 6. 返回 Token
    Map<String, Object> resp = new HashMap<>();
    resp.put("token", token);
    resp.put("expire", System.currentTimeMillis() + 2 * 3600 * 1000);
    return ResponseEntity.ok(resp);
}

6. 登录拦截器(保护需要认证的接口)

java 复制代码
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            writeError(response, "缺少认证凭证");
            return false;
        }

        token = token.substring(7); // 去掉 "Bearer "
        String userInfo = redisTemplate.opsForValue().get("login:token:" + token);
        if (userInfo == null) {
            writeError(response, "登录已过期");
            return false;
        }

        // 可选:放入 ThreadLocal
        UserContext.setCurrentUser(parseUser(userInfo));
        return true;
    }

    private void writeError(HttpServletResponse response, String msg) throws IOException {
        response.setStatus(401);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"error\":\"Unauthorized\",\"msg\":\"" + msg + "\"}");
    }
}

五、安全加固要点

风险 防御措施
验证码爆破 5分钟过期 + 一次性使用
短信轰炸 IP + 手机号双维度限流
机器刷验证码 前置图形验证码
Token 泄露 HTTPS + 不存 localStorage
会话固定 Token 随机生成(UUID 安全)

六、与传统 Session 登录对比

特性 短信登录(Redis Token) 传统 Session 登录
状态 有状态(Redis 存储) 有状态(内存/Redis)
传输 Header(Authorization) Cookie(自动)
跨域 天然支持 需配置 CORS + withCredentials
适用端 App / 小程序 / H5 传统 Web
安全性 可主动失效 可主动失效

📌 结论短信登录本质是"无密码 + Token 认证",更适合现代应用架构


七、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
科技小花2 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸2 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain2 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希3 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神3 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员3 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java3 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿3 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴3 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存
YOU OU3 小时前
三大范式和E-R图
数据库