f
This commit is contained in:
347
docs/短信接口签名验证使用指南.md
Normal file
347
docs/短信接口签名验证使用指南.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# 短信接口签名验证使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
为了防止短信发送接口被恶意刷取,系统实现了基于HMAC-SHA256的签名验证机制。所有发送短信的请求必须包含有效的签名,否则请求将被拒绝。
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **前端生成签名**:使用密钥对请求参数进行HMAC-SHA256签名
|
||||
2. **后端验证签名**:后端使用相同密钥重新计算签名并比对
|
||||
3. **时间戳验证**:防止重放攻击,时间戳必须在5分钟内有效
|
||||
4. **随机数验证**:每次请求必须包含唯一的随机字符串(nonce)
|
||||
5. **参数编码传输**(推荐):将所有参数(包括签名)编码成Base64字符串后传输,隐藏参数结构,增加安全性
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 后端配置
|
||||
|
||||
在 `config.yaml` 中配置签名相关参数:
|
||||
|
||||
```yaml
|
||||
sms:
|
||||
# ... 其他配置 ...
|
||||
# 签名验证配置
|
||||
signature_enabled: true # 是否启用签名验证
|
||||
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
|
||||
```
|
||||
|
||||
**重要提示**:
|
||||
- 生产环境必须修改 `signature_secret` 为复杂的随机字符串
|
||||
- 密钥长度建议至少32个字符
|
||||
- 密钥应包含大小写字母、数字和特殊字符
|
||||
|
||||
## 签名算法
|
||||
|
||||
### 1. 构建待签名字符串
|
||||
|
||||
将请求参数(排除`signature`字段)按key排序,拼接成以下格式:
|
||||
|
||||
```
|
||||
key1=value1&key2=value2×tamp=1234567890&nonce=random_string
|
||||
```
|
||||
|
||||
### 2. 计算HMAC-SHA256签名
|
||||
|
||||
使用配置的密钥对待签名字符串进行HMAC-SHA256计算,结果转换为hex编码。
|
||||
|
||||
### 3. 请求参数
|
||||
|
||||
系统支持两种请求方式:
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
发送请求时直接传递所有字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"phone": "13800138000",
|
||||
"scene": "register",
|
||||
"timestamp": 1704067200,
|
||||
"nonce": "a1b2c3d4e5f6g7h8",
|
||||
"signature": "abc123def456..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐,更安全)
|
||||
|
||||
将所有参数(包括签名)编码成Base64字符串后传输,只传递一个`data`字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": "eyJwaG9uZSI6IjEzODAwMTM4MDAwIiwic2NlbmUiOiJyZWdpc3RlciIsInRpbWVzdGFtcCI6MTcwNDA2NzIwMCwibm9uY2UiOiJhMWIyYzNkNGE1ZjYiLCJzaWduYXR1cmUiOiJhYmMxMjNkZWY0NTYifQ=="
|
||||
}
|
||||
```
|
||||
|
||||
**编码传输的优势**:
|
||||
- 隐藏参数结构,增加破解难度
|
||||
- 参数不可见,防止参数被直接修改
|
||||
- 增加一层编码保护
|
||||
|
||||
## 前端实现
|
||||
|
||||
### Node.js 示例
|
||||
|
||||
参考文件:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function generateSignature(params, secretKey, timestamp, nonce) {
|
||||
// 1. 构建待签名字符串
|
||||
const keys = Object.keys(params)
|
||||
.filter(k => k !== 'signature')
|
||||
.sort();
|
||||
|
||||
const parts = keys.map(k => `${k}=${params[k]}`);
|
||||
parts.push(`timestamp=${timestamp}`);
|
||||
parts.push(`nonce=${nonce}`);
|
||||
|
||||
const signString = parts.join('&');
|
||||
|
||||
// 2. 计算HMAC-SHA256签名
|
||||
const signature = crypto
|
||||
.createHmac('sha256', secretKey)
|
||||
.update(signString)
|
||||
.digest('hex');
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateRandomString(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 发送请求
|
||||
const requestBody = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐)
|
||||
|
||||
```javascript
|
||||
// 1. 生成签名(同上)
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateRandomString(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 2. 构建包含所有参数的JSON对象
|
||||
const allParams = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
|
||||
// 3. 编码为Base64
|
||||
const jsonString = JSON.stringify(allParams);
|
||||
const encodedData = Buffer.from(jsonString).toString('base64');
|
||||
|
||||
// 4. 发送请求(只传递data字段)
|
||||
const requestBody = {
|
||||
data: encodedData,
|
||||
};
|
||||
```
|
||||
|
||||
### 浏览器 JavaScript 示例
|
||||
|
||||
参考文件:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
```javascript
|
||||
async function generateSignature(params, secretKey, timestamp, nonce) {
|
||||
// 1. 构建待签名字符串
|
||||
const keys = Object.keys(params)
|
||||
.filter(k => k !== 'signature')
|
||||
.sort();
|
||||
|
||||
const parts = keys.map(k => `${k}=${params[k]}`);
|
||||
parts.push(`timestamp=${timestamp}`);
|
||||
parts.push(`nonce=${nonce}`);
|
||||
|
||||
const signString = parts.join('&');
|
||||
|
||||
// 2. 使用Web Crypto API计算HMAC-SHA256签名
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secretKey);
|
||||
const messageData = encoder.encode(signString);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
|
||||
const hashArray = Array.from(new Uint8Array(signature));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateNonce(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = await generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 发送请求
|
||||
const requestBody = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐)
|
||||
|
||||
```javascript
|
||||
// 1. 生成签名(同上)
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateNonce(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = await generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 2. 构建包含所有参数的JSON对象
|
||||
const allParams = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
|
||||
// 3. 编码为Base64(浏览器环境)
|
||||
const jsonString = JSON.stringify(allParams);
|
||||
const encodedData = btoa(unescape(encodeURIComponent(jsonString)));
|
||||
|
||||
// 4. 发送请求(只传递data字段)
|
||||
const requestBody = {
|
||||
data: encodedData,
|
||||
};
|
||||
```
|
||||
|
||||
## 密钥隐藏策略
|
||||
|
||||
由于前端代码可以被查看,完全隐藏密钥是不可能的。但可以通过以下方式增加破解难度:
|
||||
|
||||
### 1. 字符串拆分和拼接
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const part1 = 'TyApi2024';
|
||||
const part2 = 'SMSSecret';
|
||||
const part3 = 'Key!@#$%^';
|
||||
return part1 + part2 + part3;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 字符数组拼接
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const chars = ['T', 'y', 'A', 'p', 'i', ...];
|
||||
return chars.join('');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Base64编码混淆
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const encoded = 'base64_encoded_string';
|
||||
return atob(encoded);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 代码混淆
|
||||
|
||||
使用构建工具(如webpack、rollup等)进行代码混淆和压缩,使密钥更难被发现。
|
||||
|
||||
### 5. 后端代理(推荐)
|
||||
|
||||
将签名逻辑放在后端代理接口中,前端只调用代理接口,不直接包含密钥。
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **定期更换密钥**:建议每3-6个月更换一次签名密钥
|
||||
2. **监控异常请求**:监控签名验证失败的请求,及时发现攻击行为
|
||||
3. **结合其他防护措施**:
|
||||
- IP限流
|
||||
- 设备指纹识别
|
||||
- 验证码(图形验证码)
|
||||
- 行为分析
|
||||
|
||||
4. **日志记录**:记录所有签名验证失败的请求,包括IP、User-Agent等信息
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
1. **签名字段缺失**:返回 `"签名字段缺失"`
|
||||
2. **时间戳无效**:返回 `"时间戳无效"`
|
||||
3. **请求已过期**:返回 `"请求已过期,时间戳超出容差范围"`
|
||||
4. **签名验证失败**:返回 `"签名验证失败"`
|
||||
|
||||
### 时间戳容差
|
||||
|
||||
系统允许的时间戳容差为 **5分钟**(300秒)。如果请求时间戳与服务器时间差超过5分钟,请求将被拒绝。
|
||||
|
||||
## 测试
|
||||
|
||||
### 测试签名生成
|
||||
|
||||
```bash
|
||||
# 使用Node.js示例
|
||||
node tyapi-frontend/public/examples/nodejs/sms_signature_demo.js
|
||||
```
|
||||
|
||||
### 测试API调用
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/users/send-code \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone": "13800138000",
|
||||
"scene": "register",
|
||||
"timestamp": 1704067200,
|
||||
"nonce": "a1b2c3d4e5f6g7h8",
|
||||
"signature": "计算得到的签名"
|
||||
}'
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **时间同步**:确保客户端和服务器时间同步,避免时间戳验证失败
|
||||
2. **随机数唯一性**:每次请求的nonce应该是唯一的,可以使用UUID或时间戳+随机数
|
||||
3. **密钥安全**:生产环境密钥不要提交到代码仓库,应使用环境变量或密钥管理服务
|
||||
4. **向后兼容**:如果需要在开发环境禁用签名验证,可以设置 `signature_enabled: false`
|
||||
|
||||
## 相关文件
|
||||
|
||||
- 后端签名工具:`internal/shared/crypto/signature.go`
|
||||
- 后端Handler:`internal/infrastructure/http/handlers/user_handler.go`
|
||||
- 配置结构:`internal/config/config.go`
|
||||
- Node.js示例:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
|
||||
- 浏览器示例:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`
|
||||
|
||||
Reference in New Issue
Block a user