短信登录安全防护方案(Spring Boot)

文章目录

      • 一、总体设计思路
      • 二、后端限流策略
        • [2.1 限流维度与规则](#2.1 限流维度与规则)
        • [2.2 Redis 限流实现示例](#2.2 Redis 限流实现示例)
      • 三、验证码安全机制
        • [3.1 验证码基本规则](#3.1 验证码基本规则)
        • [3.2 验证码存储方式](#3.2 验证码存储方式)
        • [3.3 验证流程与防破解](#3.3 验证流程与防破解)
      • [四、图形验证码 / 滑块验证](#四、图形验证码 / 滑块验证)
        • [4.1 触发时机](#4.1 触发时机)
        • [4.2 滑块验证集成方式](#4.2 滑块验证集成方式)
      • 五、黑名单机制
        • [5.1 黑名单对象](#5.1 黑名单对象)
        • [5.2 识别规则](#5.2 识别规则)
        • [5.3 存储与查询](#5.3 存储与查询)
      • 六、接口安全:身份验证、请求签名、防重放
        • [6.1 全站 HTTPS](#6.1 全站 HTTPS)
        • [6.2 请求签名机制(建议 App 场景)](#6.2 请求签名机制(建议 App 场景))
      • 七、业务逻辑校验
        • [7.1 手机号格式验证](#7.1 手机号格式验证)
        • [7.2 归属地、运营商验证](#7.2 归属地、运营商验证)
        • [7.3 账号状态校验](#7.3 账号状态校验)
      • 八、监控告警与审计
        • [8.1 监控内容](#8.1 监控内容)
        • [8.2 告警策略](#8.2 告警策略)
        • [8.3 日志审计](#8.3 日志审计)
      • [九、缓存策略(Redis 等)](#九、缓存策略(Redis 等))
        • [9.1 使用场景](#9.1 使用场景)
        • [9.2 防止内存溢出与雪崩](#9.2 防止内存溢出与雪崩)
      • 十、数据库设计与索引
        • [10.1 短信发送日志表](#10.1 短信发送日志表)
        • [10.2 验证码校验日志表(可选)](#10.2 验证码校验日志表(可选))
        • [10.3 黑名单表](#10.3 黑名单表)
      • 十一、分布式锁:防重复发送
        • [11.1 Redis 分布式锁](#11.1 Redis 分布式锁)
      • [十二、登录流程与 JWT 集成示例](#十二、登录流程与 JWT 集成示例)
      • 十三、前端配合的安全措施
        • [13.1 请求频率限制](#13.1 请求频率限制)
        • [13.2 客户端校验](#13.2 客户端校验)
        • [13.3 安全传输与存储](#13.3 安全传输与存储)
        • [13.4 用户体验与安全提示](#13.4 用户体验与安全提示)
      • 十四、小结

一、总体设计思路

  • 目标:在支持手机号+短信验证码快捷登录的前提下,最大限度防止短信接口被批量刷号、撞库、薅羊毛和攻击,兼顾安全性、可用性和扩展性。
  • 核心手段:限流 + 验证码安全 + 图形/滑块校验 + 黑名单 + 接口签名与防重放 + 严格业务校验 + 监控告警 + 合理缓存 + 审计日志 + 分布式锁。
  • 关键技术栈:Spring Boot 3、Spring Web、Spring Validation、Spring Security(可选)、Spring Data Redis、MyBatis/JPA、HTTPs、Nginx 限流(可选)。

后端将短信登录拆分为两个接口:

  • 发送短信验证码POST /api/auth/sendSmsCode
  • 使用短信验证码登录POST /api/auth/loginBySms

二、后端限流策略

2.1 限流维度与规则

建议对以下维度分别限流,并组合判定:

  • 单手机号
    • 每分钟最多 1 次
    • 每小时最多 3 次
    • 每天最多 5~10 次(视业务而定)
  • IP 地址
    • 每分钟最多 10 次
    • 每小时最多 100 次
    • 异常区域或代理 IP 收紧阈值
  • 设备 ID / 设备指纹
    • 每分钟最多 3 次
    • 每天最多 10 次
  • 组合维度
    • phone + IPphone + deviceId,用于识别代理刷号
2.2 Redis 限流实现示例

使用 Redis 的 INCR + EXPIRE 或 Lua 脚本实现原子限流。

Redis Key 设计示例

  • sms:send:phone:{phone}:1m
  • sms:send:phone:{phone}:1h
  • sms:send:phone:{phone}:1d
  • sms:send:ip:{ip}:1m
  • sms:send:device:{deviceId}:1m

Java 拦截器/切面实现伪代码

java 复制代码
@Component
public class SmsRateLimiter {

    private final StringRedisTemplate stringRedisTemplate;

    public SmsRateLimiter(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void checkSendLimit(String phone, String ip, String deviceId) {
        // 单手机号 1 分钟一次
        assertAllowed(incrWithTtl("sms:send:phone:" + phone + ":1m", 60, 1));
        // 单手机号 1 小时 3 次
        assertAllowed(incrWithTtl("sms:send:phone:" + phone + ":1h", 3600, 3));
        // 单手机号 1 天 10 次
        assertAllowed(incrWithTtl("sms:send:phone:" + phone + ":1d", 86400, 10));
        // IP 限流
        assertAllowed(incrWithTtl("sms:send:ip:" + ip + ":1m", 60, 10));
        // 设备限流
        if (StringUtils.hasText(deviceId)) {
            assertAllowed(incrWithTtl("sms:send:dev:" + deviceId + ":1m", 60, 3));
        }
    }

    private long incrWithTtl(String key, long ttlSeconds, long max) {
        Long val = stringRedisTemplate.opsForValue().increment(key);
        if (val != null && val == 1L) {
            stringRedisTemplate.expire(key, Duration.ofSeconds(ttlSeconds));
        }
        return val == null ? 0 : val;
    }

    private void assertAllowed(long current) {
        if (current > 0 && current > 10_0000) {
            // 安全兜底,可自定义
        }
    }
}

实际代码中需要根据 key 区分不同规则,并在超过 max 时抛业务异常:

java 复制代码
if (current > max) {
    throw new BusinessException("短信发送太频繁,请稍后再试");
}

在 Controller 入口使用 @ControllerAdvice 捕获异常,返回统一错误码。


三、验证码安全机制

3.1 验证码基本规则
  • 长度与复杂度
    • 一般为 6 位数字(便于输入),高安全场景可使用数字+字母 6~8 位。
  • 有效期
    • 建议 5 分钟内有效,同时单个验证码仅允许 5 次以内的错误尝试。
  • 发送间隔
    • 与限流结合,发送后 60 秒内不允许再次发送。
3.2 验证码存储方式

推荐使用 Redis + 单向散列存储

  • Key 示例:sms:code:login:{phone}
  • Value 内容:
    • hash(code + salt)
    • 失败次数 failCount
    • 发送时间 sendTime

可用 JSON 或 Hash 结构存储。例如 Hash:

  • field: code -> 哈希值
  • field: times -> 已校验次数
  • field: ts -> 发送时间戳

生成和存储伪代码

java 复制代码
public class SmsCodeService {

    private final StringRedisTemplate redisTemplate;
    private final SecureRandom secureRandom = new SecureRandom();

    public String generateAndStoreCode(String phone) {
        String code = String.format("%06d", secureRandom.nextInt(1_000_000));
        String salt = "sms-login-salt"; // 也可每次随机,附加存储
        String hash = DigestUtils.sha256Hex(code + salt);

        String key = "sms:code:login:" + phone;
        Map<String, String> map = new HashMap<>();
        map.put("code", hash);
        map.put("times", "0");
        map.put("ts", String.valueOf(System.currentTimeMillis()));

        redisTemplate.opsForHash().putAll(key, map);
        redisTemplate.expire(key, Duration.ofMinutes(5));

        return code; // 返回给短信通道,不返回给前端
    }
}
3.3 验证流程与防破解
  • 验证步骤
    1. 从 Redis 读取验证码记录,校验是否存在及是否过期。
    2. 校验错误次数 times 是否超过 5 次,如果超过则删除记录并提示重新获取。
    3. 对用户输入的验证码做同样的 hash 比对。
    4. 验证成功后删除 Redis 记录(防止重复使用)。
  • 防暴力破解
    • 错误次数过多后,锁定该手机号一段时间(例如 30 分钟),记录黑名单风险分。
    • 结合 IP/设备 ID 统计错误率。
    • 返回错误提示要模糊化:不要区分"验证码错误"和"手机号未注册"等细节。

验证码验证伪代码

java 复制代码
public boolean verifyCode(String phone, String inputCode) {
    String key = "sms:code:login:" + phone;
    Map<Object, Object> map = redisTemplate.opsForHash().entries(key);
    if (map == null || map.isEmpty()) {
        throw new BusinessException("验证码已失效,请重新获取");
    }
    int times = Integer.parseInt((String) map.getOrDefault("times", "0"));
    if (times >= 5) {
        redisTemplate.delete(key);
        throw new BusinessException("验证码错误次数过多,请重新获取");
    }

    String salt = "sms-login-salt";
    String expectHash = (String) map.get("code");
    String actualHash = DigestUtils.sha256Hex(inputCode + salt);

    if (!actualHash.equals(expectHash)) {
        redisTemplate.opsForHash().put(key, "times", String.valueOf(times + 1));
        throw new BusinessException("验证码错误");
    }

    // 验证通过,删除
    redisTemplate.delete(key);
    return true;
}

四、图形验证码 / 滑块验证

4.1 触发时机

可配置多级触发策略:

  • 低风险:首次请求、单 IP 请求频率正常 → 不触发图形验证码。
  • 中风险:同一手机号短时间内多次请求(如 2 次以上)、同一 IP 请求过快 → 要求前端先完成人机校验(图形/滑块)。
  • 高风险:IP 命中高危段 / 代理 / 黑名单、过去 24 小时该手机号多次失败 → 强制图形验证码 + 降低限流阈值,必要时直接拒绝。

后端可以在响应中返回一个字段表示需要前端先做人机校验:

json 复制代码
{
  "code": 429,
  "message": "需要人机校验",
  "data": {
    "needCaptcha": true,
    "captchaType": "SLIDER"
  }
}
4.2 滑块验证集成方式
  • 可集成第三方服务(如极验、hCaptcha 等),前端完成交互后拿到 validate token。
  • 后端接口 sendSmsCode 需要携带该 token,并向第三方服务进行二次验证:
    • 验证成功后才继续短信发送。
    • 验证失败则拒绝并计入风险评分。

伪代码示例:

java 复制代码
public void checkCaptchaIfNeeded(String phone, String ip, String deviceId, String captchaToken) {
    boolean needCaptcha = riskService.needCaptcha(phone, ip, deviceId);
    if (!needCaptcha) {
        return;
    }
    if (!StringUtils.hasText(captchaToken)) {
        throw new BusinessException("请完成人机校验");
    }
    boolean pass = captchaClient.verify(captchaToken, ip);
    if (!pass) {
        throw new BusinessException("人机校验失败");
    }
}

五、黑名单机制

5.1 黑名单对象
  • 手机号:虚拟号段、垃圾号段、被频繁攻击的账号。
  • IP / IP 段:来自数据中心、代理、已知攻击源。
  • 设备 ID / 设备指纹:频繁撞库、批量注册设备。
5.2 识别规则
  • 结合限流数据、验证码错误率、登录失败率、行为特征(夜间大量集中请求等),为每个主体计算风险分
  • 超过阈值后写入黑名单,并设置过期时间(例如 7 天)。
  • 对于命中黑名单的请求,直接拒绝或返回"系统繁忙"。
5.3 存储与查询
  • Redis 黑名单(快速判定)
    • black:phone:{phone}
    • black:ip:{ip}
    • black:dev:{deviceId}
  • 数据库黑名单(持久化及运营管理)
    • security_blacklist,定期与 Redis 同步。

表结构示例

sql 复制代码
CREATE TABLE security_blacklist (
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    type         TINYINT NOT NULL COMMENT '1:phone,2:ip,3:device',
    value        VARCHAR(64) NOT NULL,
    reason       VARCHAR(255),
    risk_score   INT NOT NULL DEFAULT 0,
    expire_time  DATETIME,
    created_at   DATETIME NOT NULL,
    updated_at   DATETIME NOT NULL,
    UNIQUE KEY uk_type_value (type, value),
    KEY idx_expire_time (expire_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

六、接口安全:身份验证、请求签名、防重放

6.1 全站 HTTPS
  • 所有短信相关接口必须走 HTTPS,防止中间人窃听验证码。
6.2 请求签名机制(建议 App 场景)
  • 为移动 App 分配 appIdappSecret
  • 请求头携带:X-App-IdX-TimestampX-NonceX-Signature
  • 签名算法示例:
    • signature = HMAC-SHA256(appSecret, appId + timestamp + nonce + requestBody)

后端验证逻辑:

  1. 根据 appIdappSecret
  2. 校验时间戳是否在允许误差范围内(如 5 分钟)。
  3. 检查 nonce 是否在短时间内重复(在 Redis 中记录 nonce)。
  4. 使用相同算法计算签名并与 X-Signature 对比。

防重放实现示例(Redis 存储 nonce)

java 复制代码
public void validateSignature(String appId, String timestamp, String nonce, String signature, String body) {
    // 1. 校验时间戳
    long ts = Long.parseLong(timestamp);
    if (Math.abs(System.currentTimeMillis() - ts) > 5 * 60 * 1000) {
        throw new BusinessException("请求已过期");
    }

    // 2. 校验 nonce
    String nonceKey = "req:nonce:" + appId + ":" + nonce;
    Boolean success = redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", Duration.ofMinutes(10));
    if (Boolean.FALSE.equals(success)) {
        throw new BusinessException("重复请求");
    }

    // 3. 计算签名
    String appSecret = appSecretService.getSecret(appId);
    String data = appId + timestamp + nonce + body;
    String expected = hmacSha256Hex(appSecret, data);
    if (!expected.equalsIgnoreCase(signature)) {
        throw new BusinessException("签名错误");
    }
}

七、业务逻辑校验

7.1 手机号格式验证
  • 使用正则在后端强校验:
java 复制代码
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;

结合 Spring Validation:在 Controller 入参 DTO 上增加注解。

7.2 归属地、运营商验证
  • 使用本地号段库或第三方服务按需校验:
    • 过滤不支持的国家/地区号段。
    • 对高风险国家号段可直接拒绝或收紧限流。
  • 可增加 phone_region 表存储号段归属信息,定期更新。
7.3 账号状态校验
  • 登录前检查该手机号对应账号是否处于:
    • 冻结、注销、风险控制状态。
  • 风控状态下可要求额外的图形验证码或密码验证。

八、监控告警与审计

8.1 监控内容
  • 短信发送量:按分钟/小时/天统计,按通道、按国家/地区拆分。
  • 发送成功率和失败原因:区分运营商失败/限流/黑名单/第三方通道错误。
  • 验证码验证失败率:按手机号、IP、设备聚合。
  • 黑名单增长情况:黑名单新增数量、命中次数。
8.2 告警策略
  • 单位时间内短信发送量激增或成功率骤降 → 通知运维与安全。
  • 单 IP / 单设备 / 单号段在短时间内请求激增 → 风控告警。
  • 验证码错误率异常偏高 → 可能存在撞库攻击。

落地方案:

  • 使用日志(Logback)输出关键行为到专用索引字段,接入 ELK / Loki + Grafana。
  • 配合 Prometheus 指标(Counter, Gauge),设置告警规则对接钉钉/企业微信/飞书。
8.3 日志审计
  • 记录但脱敏:手机号只保留前 3 位 + 后 4 位,例如 138****1234
  • 日志中禁止记录明文验证码和完整身份证号等敏感信息。

九、缓存策略(Redis 等)

9.1 使用场景
  • 验证码存储 (sms:code:login:{phone})。
  • 限流计数 (sms:send:phone:{phone}:1m 等)。
  • 黑名单快速判定 (black:phone:{phone})。
  • 请求签名防重放 nonce (req:nonce:{appId}:{nonce})。
9.2 防止内存溢出与雪崩
  • TTL 策略:所有短信相关 key 必须设置 TTL,且 ttl 不宜过长。
  • Key 前缀规范 :便于运维统计和清理(如 sms:*black:*)。
  • 限流计数使用短 TTL 的整数,不存储大对象。
  • 最大内存策略 :在 Redis 中配置 maxmemory 和适当的淘汰策略(如 allkeys-lru),并监控内存使用率。
  • 降级方案
    • Redis 不可用时,可以暂时禁止短信发送,防止在无法限流时被恶意刷号。

十、数据库设计与索引

10.1 短信发送日志表

用于审计和风控分析,不建议直接作为限流依据(限流放在 Redis)。

sql 复制代码
CREATE TABLE sms_send_log (
    id            BIGINT PRIMARY KEY AUTO_INCREMENT,
    phone         VARCHAR(32) NOT NULL,
    ip            VARCHAR(64),
    device_id     VARCHAR(64),
    scene         VARCHAR(32) NOT NULL COMMENT 'login, register 等',
    template_code VARCHAR(64) NOT NULL,
    content       VARCHAR(512) NOT NULL,
    status        TINYINT NOT NULL COMMENT '0:待发送,1:成功,2:失败',
    fail_reason   VARCHAR(255),
    channel       VARCHAR(32) COMMENT '短信通道商',
    created_at    DATETIME NOT NULL,
    sent_at       DATETIME,
    INDEX idx_phone_scene_time (phone, scene, created_at),
    INDEX idx_ip_time (ip, created_at),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 可按时间(如按月)进行分表(结合 MySQL 分区/分表功能)。
10.2 验证码校验日志表(可选)

记录验证码验证失败情况,用于风控建模。

sql 复制代码
CREATE TABLE sms_verify_log (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    phone       VARCHAR(32) NOT NULL,
    ip          VARCHAR(64),
    device_id   VARCHAR(64),
    scene       VARCHAR(32) NOT NULL,
    success     TINYINT NOT NULL,
    reason      VARCHAR(255),
    created_at  DATETIME NOT NULL,
    INDEX idx_phone_time (phone, created_at),
    INDEX idx_ip_time (ip, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
10.3 黑名单表

参考前文 security_blacklist 设计,增加索引 uk_type_value 用于快速查找。


十一、分布式锁:防重复发送

在高并发场景下,需要避免同一手机号在同一时间窗口内被多次发送验证码。

11.1 Redis 分布式锁
  • 锁 Key:lock:sms:send:{phone},锁过期时间设置为 5~10 秒。
  • 发送验证码前先抢锁,成功后执行发送逻辑,最后释放锁(或等待过期)。

伪代码示例:

java 复制代码
public void sendSmsCode(String phone, String ip, String deviceId, String captchaToken) {
    String lockKey = "lock:sms:send:" + phone;
    String lockVal = UUID.randomUUID().toString();
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockVal, Duration.ofSeconds(10));
    if (Boolean.FALSE.equals(locked)) {
        throw new BusinessException("短信发送中,请稍后再试");
    }
    try {
        // 1. 黑名单校验
        blacklistService.check(phone, ip, deviceId);
        // 2. 限流校验
        smsRateLimiter.checkSendLimit(phone, ip, deviceId);
        // 3. 图形/滑块验证
        checkCaptchaIfNeeded(phone, ip, deviceId, captchaToken);
        // 4. 生成并存储验证码
        String code = smsCodeService.generateAndStoreCode(phone);
        // 5. 调用第三方短信通道发送
        smsChannelClient.sendLoginCode(phone, code);
        // 6. 记录日志
        smsLogService.recordSendLog(...);
    } finally {
        // 释放锁(注意只释放自己持有的锁)
        String val = redisTemplate.opsForValue().get(lockKey);
        if (lockVal.equals(val)) {
            redisTemplate.delete(lockKey);
        }
    }
}

生产环境可优先考虑使用 Redisson 等成熟组件实现分布式锁。


十二、登录流程与 JWT 集成示例

短信登录成功后,一般会签发一个访问令牌(如 JWT),后续接口基于令牌鉴权。

登录接口伪代码

java 复制代码
@PostMapping("/api/auth/loginBySms")
public Result<LoginResp> loginBySms(@Valid @RequestBody SmsLoginReq req, HttpServletRequest httpRequest) {
    String ip = IpUtils.getClientIp(httpRequest);
    String deviceId = req.getDeviceId();

    // 1. 黑名单 & 风控
    blacklistService.check(req.getPhone(), ip, deviceId);

    // 2. 验证短信验证码
    smsCodeService.verifyCode(req.getPhone(), req.getCode());

    // 3. 获取或创建用户
    User user = userService.getOrCreateByPhone(req.getPhone());

    // 4. 生成 JWT
    String token = jwtUtil.generateToken(user.getId(), user.getRole());

    // 5. 记录登录日志
    loginLogService.recordLogin(user.getId(), ip, deviceId, "SMS");

    LoginResp resp = new LoginResp();
    resp.setToken(token);
    resp.setUserId(user.getId());
    return Result.success(resp);
}

JWT 本身也要注意:

  • 使用足够长度的对称秘钥或非对称密钥。
  • 设置合理过期时间(如 2 小时),并结合刷新令牌机制。
  • 优先在服务端维护 Token 黑名单(踢下线时使用 Redis 存储失效 token 的 jti)。

十三、前端配合的安全措施

13.1 请求频率限制
  • 点击"获取验证码"后,按钮进入倒计时状态(如 60 秒),期间禁止再次点击。
  • 对同一页面或设备上的连续操作做防抖/节流处理(例如 1~2 秒内禁止多次触发)。
13.2 客户端校验
  • 在前端进行手机号格式校验(正则),避免无效请求直达服务端。
  • 对登录页面进行基础防刷:
    • 使用图形/滑块验证码组件。
    • 随机化部分重要字段的名称/顺序增加脚本抓取难度(有限防护)。
13.3 安全传输与存储
  • 前端必须通过 HTTPS 发起请求。
  • 不在浏览器本地持久化存储验证码,不在控制台或日志输出验证码。
  • JWT 建议使用 Authorization: Bearer xxx 头传输,避免放在 URL 参数。
13.4 用户体验与安全提示
  • 对频繁操作的用户给出友好提示,不暴露系统内部风控策略。
  • 登录成功后,对新设备/新地区登录可提示用户注意账户安全。

十四、小结

通过 Redis 限流 + 验证码安全存储 + 图形/滑块验证码 + 黑名单 + 签名与防重放 + 严格业务校验 + 监控告警 + 分布式锁 等组合手段,可以大幅降低短信登录接口被恶意盗刷和滥用的风险。

在 Spring Boot 项目中落地时,需要:

  • 封装统一的限流与风控组件,避免散落在各个 Controller 中。
  • 统一错误码与返回结构,避免向攻击者泄露过多信息。
  • 定期复盘日志与监控指标,持续优化规则与阈值。
相关推荐
古城小栈2 小时前
Tokio:Rust 异步界的 “霸主”
开发语言·后端·rust
_OP_CHEN2 小时前
【从零开始的Qt开发指南】(二十)Qt 多线程深度实战指南:从基础 API 到线程安全,带你实现高效并发应用
开发语言·c++·qt·安全·线程·前端开发·线程安全
进击的丸子2 小时前
基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践
java·后端·github
while(1){yan}2 小时前
SpringAOP
java·开发语言·spring boot·spring·aop
techdashen2 小时前
Go 1.18+ slice 扩容机制详解
开发语言·后端·golang
浙江巨川-吉鹏2 小时前
【城市地表水位连续监测自动化系统】沃思智能
java·后端·struts·城市地表水位连续监测自动化系统·地表水位监测系统
小李独爱秋2 小时前
计算机网络经典问题透视:端到端时延和时延抖动有什么区别?
运维·服务器·计算机网络·安全·web安全
Arwen3032 小时前
如何消除APP、软件的不安全下载提示?怎样快速申请代码签名证书?
网络·网络协议·tcp/ip·安全·php·ssl
前端开发与ui设计的老司机2 小时前
数字孪生的“瘦身术”与“安全舱”:模型轻量化与模型降级全解析
安全·轻量化·模型降级