9.5 KiB
9.5 KiB
短信接口签名验证使用指南
概述
为了防止短信发送接口被恶意刷取,系统实现了基于HMAC-SHA256的签名验证机制。所有发送短信的请求必须包含有效的签名,否则请求将被拒绝。
工作原理
- 前端生成签名:使用密钥对请求参数进行HMAC-SHA256签名
- 后端验证签名:后端使用相同密钥重新计算签名并比对
- 时间戳验证:防止重放攻击,时间戳必须在5分钟内有效
- 随机数验证:每次请求必须包含唯一的随机字符串(nonce)
- 参数编码传输(推荐):将所有参数(包括签名)编码成Base64字符串后传输,隐藏参数结构,增加安全性
配置说明
后端配置
在 config.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:直接传递参数
发送请求时直接传递所有字段:
{
"phone": "13800138000",
"scene": "register",
"timestamp": 1704067200,
"nonce": "a1b2c3d4e5f6g7h8",
"signature": "abc123def456..."
}
方式2:编码后传输(推荐,更安全)
将所有参数(包括签名)编码成Base64字符串后传输,只传递一个data字段:
{
"data": "eyJwaG9uZSI6IjEzODAwMTM4MDAwIiwic2NlbmUiOiJyZWdpc3RlciIsInRpbWVzdGFtcCI6MTcwNDA2NzIwMCwibm9uY2UiOiJhMWIyYzNkNGE1ZjYiLCJzaWduYXR1cmUiOiJhYmMxMjNkZWY0NTYifQ=="
}
编码传输的优势:
- 隐藏参数结构,增加破解难度
- 参数不可见,防止参数被直接修改
- 增加一层编码保护
前端实现
Node.js 示例
参考文件:tyapi-frontend/public/examples/nodejs/sms_signature_demo.js
方式1:直接传递参数
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:编码后传输(推荐)
// 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:直接传递参数
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:编码后传输(推荐)
// 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. 字符串拆分和拼接
function getSecretKey() {
const part1 = 'TyApi2024';
const part2 = 'SMSSecret';
const part3 = 'Key!@#$%^';
return part1 + part2 + part3;
}
2. 字符数组拼接
function getSecretKey() {
const chars = ['T', 'y', 'A', 'p', 'i', ...];
return chars.join('');
}
3. Base64编码混淆
function getSecretKey() {
const encoded = 'base64_encoded_string';
return atob(encoded);
}
4. 代码混淆
使用构建工具(如webpack、rollup等)进行代码混淆和压缩,使密钥更难被发现。
5. 后端代理(推荐)
将签名逻辑放在后端代理接口中,前端只调用代理接口,不直接包含密钥。
安全建议
-
定期更换密钥:建议每3-6个月更换一次签名密钥
-
监控异常请求:监控签名验证失败的请求,及时发现攻击行为
-
结合其他防护措施:
- IP限流
- 设备指纹识别
- 验证码(图形验证码)
- 行为分析
-
日志记录:记录所有签名验证失败的请求,包括IP、User-Agent等信息
错误处理
常见错误
- 签名字段缺失:返回
"签名字段缺失" - 时间戳无效:返回
"时间戳无效" - 请求已过期:返回
"请求已过期,时间戳超出容差范围" - 签名验证失败:返回
"签名验证失败"
时间戳容差
系统允许的时间戳容差为 5分钟(300秒)。如果请求时间戳与服务器时间差超过5分钟,请求将被拒绝。
测试
测试签名生成
# 使用Node.js示例
node tyapi-frontend/public/examples/nodejs/sms_signature_demo.js
测试API调用
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": "计算得到的签名"
}'
注意事项
- 时间同步:确保客户端和服务器时间同步,避免时间戳验证失败
- 随机数唯一性:每次请求的nonce应该是唯一的,可以使用UUID或时间戳+随机数
- 密钥安全:生产环境密钥不要提交到代码仓库,应使用环境变量或密钥管理服务
- 向后兼容:如果需要在开发环境禁用签名验证,可以设置
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