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