Files
tyapi-server/docs/短信接口签名验证使用指南.md
2026-02-12 13:27:08 +08:00

348 lines
9.5 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.

# 短信接口签名验证使用指南
## 概述
为了防止短信发送接口被恶意刷取系统实现了基于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&timestamp=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`