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

328 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 验证码安全机制说明
## 安全问题
### 旧方案的漏洞
```
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接口变化
#### 发送验证码接口
**旧版本**
```json
POST /auth/send-sms-code
Request: { "phone": "13800138000" }
Response: {
"code": 200,
"message": "验证码已发送",
"data": true
}
```
**新版本**
```json
POST /auth/send-sms-code
Request: { "phone": "13800138000" }
Response: {
"code": 200,
"message": "验证码已发送",
"data": {
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"message": "验证码已发送"
}
}
```
#### 注册接口
**旧版本**
```json
POST /auth/register
{
"registerType": "phone",
"phone": "13800138000",
"smsCode": "123456",
"password": "123456",
"confirmPassword": "123456"
}
```
**新版本**
```json
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
```typescript
// 发送验证码
const sendSmsCode = async () => {
const result = await authApi.sendSmsCode(phone);
if (result.code === 200 && result.data) {
// 保存sessionId到表单
registerForm.smsSessionId = result.data.sessionId;
}
};
```
#### 2. 提交注册
```typescript
// 注册
const register = async () => {
const result = await authApi.register({
registerType: 'phone',
phone: '13800138000',
smsCode: '123456',
smsSessionId: registerForm.smsSessionId, // 传递sessionId
password: '123456',
confirmPassword: '123456'
});
};
```
### 后端实现
#### 1. 生成SessionID
```java
// 生成会话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
```java
// 获取存储的值
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机制我们将验证码从"谁都可以用"变成了"只有请求者可以用",大大提高了系统的安全性。这是一个简单但有效的安全增强方案,推荐在所有需要验证码的场景中使用。