Files
schoolNews/schoolNewsServ/auth/验证码安全机制说明.md

7.9 KiB
Raw Permalink Blame History

验证码安全机制说明

安全问题

旧方案的漏洞

Redis存储sms:code:手机号 => 验证码

问题:任何人只要知道手机号和验证码,就可以注册该手机号的账号。

攻击场景

  1. 攻击者获取目标用户手机号
  2. 攻击者触发验证码发送
  3. 如果攻击者通过其他方式获取到验证码(如社会工程学、短信拦截等)
  4. 攻击者可以用这个手机号+验证码注册账号

新方案SessionID绑定

核心思想

验证码不直接绑定手机号/邮箱而是绑定一个临时会话IDSessionID确保只有发起验证码请求的用户才能使用。

实现机制

1. 发送验证码流程

用户请求发送验证码(手机号/邮箱)
          ↓
后端生成 SessionIDUUID
          ↓
发送验证码到用户手机/邮箱
          ↓
Redis存储
  key: sms:code:SessionID
  value: 手机号:验证码
  expire: 5分钟
          ↓
返回 SessionID 给前端

关键点

  • SessionID 是随机生成的UUID无法预测
  • 验证码存储时绑定SessionID不直接绑定手机号
  • 前端必须保存并传递SessionID

2. 验证验证码流程

用户提交注册表单
  - 手机号/邮箱
  - 验证码
  - SessionID从发送验证码时获取
          ↓
后端验证流程:
  1. 通过SessionID从Redis获取手机号:验证码
  2. 验证提交的手机号是否匹配存储的手机号
  3. 验证提交的验证码是否匹配存储的验证码
  4. 两者都匹配才允许注册
          ↓
注册成功后删除验证码

数据结构

Redis存储格式

短信验证码

Key:   sms:code:550e8400-e29b-41d4-a716-446655440000
Value: 13800138000:123456
TTL:   300秒5分钟

邮箱验证码

Key:   email:code:650e8400-e29b-41d4-a716-446655440001
Value: user@example.com:654321
TTL:   300秒5分钟

频率限制

Key:   sms:code:ratelimit:13800138000
Value: 1
TTL:   300秒5分钟

API接口变化

发送验证码接口

旧版本

POST /auth/send-sms-code
Request: { "phone": "13800138000" }
Response: { 
  "code": 200, 
  "message": "验证码已发送", 
  "data": true 
}

新版本

POST /auth/send-sms-code
Request: { "phone": "13800138000" }
Response: { 
  "code": 200, 
  "message": "验证码已发送", 
  "data": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "message": "验证码已发送"
  }
}

注册接口

旧版本

POST /auth/register
{
  "registerType": "phone",
  "phone": "13800138000",
  "smsCode": "123456",
  "password": "123456",
  "confirmPassword": "123456"
}

新版本

POST /auth/register
{
  "registerType": "phone",
  "phone": "13800138000",
  "smsCode": "123456",
  "smsSessionId": "550e8400-e29b-41d4-a716-446655440000",  // 新增
  "password": "123456",
  "confirmPassword": "123456"
}

安全优势

1. 防止验证码盗用

  • 旧方案:知道手机号+验证码 = 可以注册
  • 新方案:需要手机号+验证码+SessionID = 更安全

攻击者即使获取到验证码没有SessionID也无法注册。

2. 会话绑定

SessionID由发起验证码请求的用户持有其他人无法获取。

3. 防止批量注册

每次发送验证码都会生成新的SessionID无法重复使用。

4. 时效性

SessionID和验证码都有5分钟有效期过期自动失效。

攻击场景分析

场景1短信拦截攻击

攻击流程

  1. 攻击者获取目标手机号
  2. 攻击者拦截短信获取验证码

旧方案:✗ 攻击者可以注册 新方案:✓ 攻击者没有SessionID无法注册

场景2社会工程学

攻击流程

  1. 攻击者诱骗用户说出验证码

旧方案:✗ 攻击者可以注册 新方案:✓ 攻击者没有SessionID无法注册

场景3中间人攻击

攻击流程

  1. 攻击者拦截网络请求获取验证码

旧方案:✗ 攻击者可以注册 新方案:✗ 攻击者可以拦截SessionID需要HTTPS

防御必须使用HTTPS加密传输

前端实现

1. 保存SessionID

// 发送验证码
const sendSmsCode = async () => {
  const result = await authApi.sendSmsCode(phone);
  if (result.code === 200 && result.data) {
    // 保存sessionId到表单
    registerForm.smsSessionId = result.data.sessionId;
  }
};

2. 提交注册

// 注册
const register = async () => {
  const result = await authApi.register({
    registerType: 'phone',
    phone: '13800138000',
    smsCode: '123456',
    smsSessionId: registerForm.smsSessionId,  // 传递sessionId
    password: '123456',
    confirmPassword: '123456'
  });
};

后端实现

1. 生成SessionID

// 生成会话ID
String sessionId = IDUtils.generateID();

// 存储验证码绑定sessionId
String codeKey = "sms:code:" + sessionId;
String codeValue = phone + ":" + code;
redisService.set(codeKey, codeValue, 5, TimeUnit.MINUTES);

// 返回sessionId
Map<String, String> data = Map.of(
    "sessionId", sessionId,
    "message", "验证码已发送"
);

2. 验证SessionID

// 获取存储的值
String codeKey = "sms:code:" + smsSessionId;
String storedValue = redisService.get(codeKey);

// 解析:手机号:验证码
String[] parts = storedValue.split(":");
String storedPhone = parts[0];
String storedCode = parts[1];

// 验证
if (!storedPhone.equals(phone)) {
    return "手机号与验证码不匹配";
}
if (!storedCode.equals(smsCode)) {
    return "验证码错误";
}

// 验证通过后删除
redisService.delete(codeKey);

验证日志

[INFO] 短信验证码已发送,手机号: 138****8000, sessionId: 550e8400-e29b-41d4-a716-446655440000
[INFO] 手机号注册: 13800138000, sessionId: 550e8400-e29b-41d4-a716-446655440000
[WARN] 手机号注册验证失败,提交手机号: 13800138001, 验证码绑定手机号: 13800138000

注意事项

  1. HTTPS必须

    • SessionID通过HTTP传输必须使用HTTPS加密
    • 防止中间人攻击窃取SessionID
  2. SessionID保密

    • 不要在日志中完整输出SessionID
    • 不要在URL参数中传递SessionID
  3. 有效期设置

    • 验证码有效期5分钟
    • 频率限制5分钟内只能发送一次
    • SessionID随验证码一起失效
  4. 错误提示

    • 不要暴露具体的验证失败原因
    • 统一返回"验证码错误"或"验证码已过期"
  5. 前端存储

    • SessionID存储在组件状态中不持久化
    • 页面刷新后SessionID丢失需要重新获取验证码

对比总结

方面 旧方案 新方案 优势
存储键 手机号 SessionID 无法通过手机号直接访问
验证要素 手机号+验证码 手机号+验证码+SessionID 多一层安全保障
防止盗用 SessionID由请求方持有
批量攻击 易受攻击 难以攻击 每次请求生成新SessionID
实现复杂度 简单 中等 增加SessionID管理
前端改动 需要保存SessionID 增加一个字段

推荐实践

  1. 使用HTTPS强制要求HTTPS访问
  2. SessionID加密可以考虑对SessionID进行签名
  3. 监控异常:记录验证失败的日志,监控异常行为
  4. IP限流限制单个IP的发送频率
  5. 图形验证码:发送验证码前增加图形验证码
  6. 行为分析:分析用户行为,识别机器人
  7. 黑名单机制:记录恶意手机号/IP加入黑名单

总结

通过引入SessionID机制我们将验证码从"谁都可以用"变成了"只有请求者可以用",大大提高了系统的安全性。这是一个简单但有效的安全增强方案,推荐在所有需要验证码的场景中使用。