敏感 API 接口如何增加短信验证码二次鉴权防止越权操作?

文章导读
给敏感 API 加短信验证码二次鉴权,核心是把它用在“高风险操作”确认上,主要用于防御会话劫持后的恶意操作,而非修补所有的逻辑越权漏洞。适用场景主要是修改密码、提现、变更绑定手机等关键业务。
📋 目录
  1. A 后端代码实现示例(Java Spring Boot)
  2. B Redis 状态机设计
  3. C 短信接口限流配置
  4. D 前端交互逻辑
  5. E 验证与排查
  6. F 常见风险与规避
  7. G 参考来源
A A

给敏感 API 加短信验证码二次鉴权,核心是把它用在“高风险操作”确认上,主要用于防御会话劫持后的恶意操作,而非修补所有的逻辑越权漏洞。适用场景主要是修改密码、提现、变更绑定手机等关键业务。

核心要点:短信二次鉴权能有效增加攻击成本,但必须配合服务端状态机控制,否则容易被绕过。注意:此方案主要防止攻击者利用窃取的 Cookie/Token 执行操作,无法防止业务逻辑层面的 IDOR(如 A 用户修改参数操作 B 用户数据),后者需在业务逻辑层做权限校验。

  • 先判断:梳理出真正涉及资金、隐私或权限变更的接口,不要全量开启。
  • 优先做:在服务端建立“待验证”状态,验证通过前不执行实际业务逻辑。
  • 再验证:测试验证码重放、过期、暴力破解以及未验证直接调用业务接口的情况。

后端代码实现示例(Java Spring Boot)

实现该功能需要后端业务逻辑改造,以下是基于 Java Spring Boot 的具体代码实现方案。核心是将“发起请求”与“确认执行”拆分为两个接口,中间通过 Redis 存储临时状态。

// 1. 发起敏感操作请求(生成 Ticket 并发送短信)
@PostMapping("/api/requestWithdraw")
public ResponseEntity<TicketResponse> requestWithdraw(@RequestBody WithdrawRequest req) {
    // 校验用户登录态
    User user = userService.getCurrentUser();
    
    // 生成临时 Ticket
    String ticket = UUID.randomUUID().toString();
    
    // 构建待执行任务数据
    PendingTask task = new PendingTask();
    task.setAction("withdraw");
    task.setUserId(user.getId());
    task.setAmount(req.getAmount());
    task.setExpireTime(System.currentTimeMillis() + 300000); // 5 分钟过期
    
    // 存入 Redis,设置过期时间
    redisTemplate.opsForValue().setEx("sensitve_op:" + ticket, 300, task);
    
    // 发送短信(此处应集成限流逻辑)
    smsService.sendVerifyCode(user.getPhone());
    
    return ResponseEntity.ok(new TicketResponse(ticket));
}

// 2. 提交验证码确认执行
@PostMapping("/api/confirmWithdraw")
public ResponseEntity<Void> confirmWithdraw(@RequestBody ConfirmRequest req) {
    String key = "sensitve_op:" + req.getTicket();
    PendingTask task = (PendingTask) redisTemplate.opsForValue().get(key);
    
    if (task == null) {
        throw new BusinessException("操作已过期或 Ticket 无效");
    }
    
    // 校验验证码
    if (!smsService.verifyCode(task.getUserId(), req.getCode())) {
        throw new BusinessException("验证码错误");
    }
    
    // 执行实际业务逻辑
    withdrawService.execute(task.getUserId(), task.getAmount());
    
    // 立即删除 Ticket,防止重放
    redisTemplate.delete(key);
    
    return ResponseEntity.ok().build();
}

Redis 状态机设计

不要直接在原接口加验证,要利用 Redis 创建临时任务。Key 的设计建议包含业务前缀,便于管理和清理。

# 存储命令示例
SETEX sensitve_op:{ticket} 300 "{\"action\":\"withdraw\",\"amount\":100,\"userId\":1001}"

# 查询命令示例
GET sensitve_op:{ticket}

# 删除命令示例(执行成功后)
DEL sensitve_op:{ticket}

短信接口限流配置

发送接口未做限流,可能被恶意调用导致资损或骚扰用户。建议结合 IP 和用户维度进行限制。

# Redis Lua 脚本限流逻辑示例
# Key: sms_limit:{phone} 或 sms_limit:{ip}
# 限制:同一手机号 60 秒内只能发 1 条,每天最多 10 条

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
    redis.call('EXPIRE', key, expire)
end
if current > limit then
    return 0
end
return 1

前端交互逻辑

前端需配合后端状态机,避免用户重复提交或困惑。

  • 发起请求后:禁用提交按钮,显示“验证码已发送”,启动 60 秒倒计时。
  • 确认阶段:输入验证码后,再次点击确认前校验格式,提交期间显示 Loading 状态。
  • 异常处理:若返回 Ticket 失效,引导用户重新发起流程,而不是单纯刷新页面。

验证与排查

部署后需通过以下命令验证安全策略是否生效。

1. 抓包重放测试

# 捕获确认操作请求,修改验证码或 ticket,服务端应返回 400 或 401
curl -X POST http://api.example.com/api/confirmWithdraw \
  -H "Content-Type: application/json" \
  -d "{\"ticket\":\"valid_ticket\", \"code\":\"999999\"}"

2. 绕过测试

# 尝试直接调用最终业务接口(跳过验证码步骤),服务端应拒绝
curl -X POST http://api.example.com/api/executeWithdraw \
  -H "Authorization: Bearer {token}" \
  -d "{\"amount\":100}"

3. 时效测试

等待验证码过期(如 5 分钟)后再提交,应提示“操作已过期”。

敏感 API 接口如何增加短信验证码二次鉴权防止越权操作?

4. 日志检查

查看服务端安全日志,确认是否有记录验证码发送、校验成功/失败的审计日志,便于追溯异常行为。

常见风险与规避

1. 验证码可暴力破解

如果验证码是 4 位数字且无尝试次数限制,攻击者可脚本爆破。建议:验证码至少 6 位,且连续错误 5 次后锁定该 Ticket 或手机号。

2. 短信轰炸风险

发送接口未做限流。建议:实施上述 Redis 限流策略,并接入第三方风控服务识别恶意 IP。

3. 用户体验断层

频繁弹出短信验证会导致用户流失。建议:结合风险控制系统,仅在异常环境(新设备、异地 IP)或高风险操作时触发。

4. SIM 卡劫持

短信并非绝对安全,存在 SIM 卡克隆或运营商内部风险。极高敏感场景(如大额转账)建议结合 Authenticator App 或人脸验证。

参考来源

  • OWASP, "Authentication Cheat Sheet", https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html