登录注册、手机号、邮箱
This commit is contained in:
327
schoolNewsServ/auth/验证码安全机制说明.md
Normal file
327
schoolNewsServ/auth/验证码安全机制说明.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# 验证码安全机制说明
|
||||
|
||||
## 安全问题
|
||||
|
||||
### 旧方案的漏洞
|
||||
|
||||
```
|
||||
Redis存储:sms:code:手机号 => 验证码
|
||||
```
|
||||
|
||||
**问题**:任何人只要知道手机号和验证码,就可以注册该手机号的账号。
|
||||
|
||||
**攻击场景**:
|
||||
1. 攻击者获取目标用户手机号
|
||||
2. 攻击者触发验证码发送
|
||||
3. 如果攻击者通过其他方式获取到验证码(如社会工程学、短信拦截等)
|
||||
4. 攻击者可以用这个手机号+验证码注册账号
|
||||
|
||||
## 新方案:SessionID绑定
|
||||
|
||||
### 核心思想
|
||||
|
||||
验证码不直接绑定手机号/邮箱,而是绑定一个临时会话ID(SessionID),确保只有发起验证码请求的用户才能使用。
|
||||
|
||||
### 实现机制
|
||||
|
||||
#### 1. 发送验证码流程
|
||||
|
||||
```
|
||||
用户请求发送验证码(手机号/邮箱)
|
||||
↓
|
||||
后端生成 SessionID(UUID)
|
||||
↓
|
||||
发送验证码到用户手机/邮箱
|
||||
↓
|
||||
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机制,我们将验证码从"谁都可以用"变成了"只有请求者可以用",大大提高了系统的安全性。这是一个简单但有效的安全增强方案,推荐在所有需要验证码的场景中使用。
|
||||
|
||||
Reference in New Issue
Block a user