/** * 短信发送接口签名示例 * * 本示例演示如何为短信发送接口生成HMAC-SHA256签名 * * 安全提示: * 1. 密钥应该通过代码混淆、字符串拆分等方式隐藏 * 2. 不要在前端代码中直接暴露完整密钥 * 3. 建议使用构建工具进行代码混淆 */ const crypto = require('crypto'); /** * 获取签名密钥(通过多种方式混淆,增加破解难度) * 注意:这只是示例,实际使用时应该进一步混淆 */ function getSecretKey() { // 方式1: 字符串拆分和拼接 const part1 = 'TyApi2024'; const part2 = 'SMSSecret'; const part3 = 'Key!@#$%^'; const part4 = '&*()_+QWERTY'; const part5 = 'UIOP'; // 方式2: Base64解码(可选,增加一层混淆) // const encoded = Buffer.from('some_base64_string', 'base64').toString(); // 方式3: 字符数组拼接 const chars = ['T', 'y', 'A', 'p', 'i', '2', '0', '2', '4', 'S', 'M', 'S', 'S', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P']; const fromChars = chars.join(''); // 组合多种方式(实际密钥:TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP) return part1 + part2 + part3 + part4 + part5; } /** * 生成随机字符串(用于nonce) */ function generateNonce(length = 16) { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } /** * 生成HMAC-SHA256签名 * * @param {Object} params - 请求参数对象 * @param {string} secretKey - 签名密钥 * @param {number} timestamp - 时间戳(秒) * @param {string} nonce - 随机字符串 * @returns {string} 签名字符串(hex编码) */ function generateSignature(params, secretKey, timestamp, nonce) { // 1. 构建待签名字符串:按key排序,拼接成 key1=value1&key2=value2 格式 const keys = Object.keys(params) .filter(k => k !== 'signature') // 排除签名字段 .sort(); const parts = keys.map(k => `${k}=${params[k]}`); // 2. 添加时间戳和随机数 parts.push(`timestamp=${timestamp}`); parts.push(`nonce=${nonce}`); // 3. 拼接成待签名字符串 const signString = parts.join('&'); // 4. 使用HMAC-SHA256计算签名 const signature = crypto .createHmac('sha256', secretKey) .update(signString) .digest('hex'); return signature; } /** * 自定义编码字符集(与后端保持一致) */ const CUSTOM_ENCODE_CHARSET = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"; /** * 自定义Base64编码(使用自定义字符集) */ function customBase64Encode(data) { if (data.length === 0) return ''; const bytes = Buffer.from(data, 'utf8'); const charset = CUSTOM_ENCODE_CHARSET; let result = ''; // 将3个字节(24位)编码为4个字符 for (let i = 0; i < bytes.length; i += 3) { const b1 = bytes[i]; const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0; const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0; // 组合成24位 const combined = (b1 << 16) | (b2 << 8) | b3; // 分成4个6位段 result += charset[(combined >> 18) & 0x3F]; result += charset[(combined >> 12) & 0x3F]; if (i + 1 < bytes.length) { result += charset[(combined >> 6) & 0x3F]; } else { result += '='; // 填充字符 } if (i + 2 < bytes.length) { result += charset[combined & 0x3F]; } else { result += '='; // 填充字符 } } return result; } /** * 应用字符偏移混淆 */ function applyCharShift(data, shift) { const charset = CUSTOM_ENCODE_CHARSET; const charsetLen = charset.length; let result = ''; for (let i = 0; i < data.length; i++) { const c = data[i]; if (c === '=') { result += c; // 填充字符不变 continue; } const idx = charset.indexOf(c); if (idx === -1) { result += c; // 不在字符集中,保持不变 } else { // 应用偏移 const newIdx = (idx + shift) % charsetLen; result += charset[newIdx]; } } return result; } /** * 自定义编码请求数据 */ function encodeRequest(data) { // 1. 使用自定义Base64编码 const encoded = customBase64Encode(data); // 2. 应用字符偏移混淆(偏移7个位置) const confused = applyCharShift(encoded, 7); return confused; } /** * 发送短信验证码(带签名)- 方式2:编码后传输(推荐,更安全) * 将所有参数(包括签名)使用自定义编码方案编码后传输,隐藏参数结构 * * @param {string} phone - 手机号 * @param {string} scene - 场景(register/login/change_password/reset_password等) * @param {string} apiBaseUrl - API基础URL * @returns {Promise} 响应结果 */ async function sendSMSWithEncodedSignature(phone, scene, apiBaseUrl = 'http://localhost:8080') { // 1. 准备参数 const timestamp = Math.floor(Date.now() / 1000); // 当前时间戳(秒) const nonce = generateNonce(16); // 生成随机字符串 const params = { phone: phone, scene: scene, }; // 2. 生成签名 const secretKey = getSecretKey(); const signature = generateSignature(params, secretKey, timestamp, nonce); // 3. 构建包含所有参数的JSON对象 const allParams = { phone: phone, scene: scene, timestamp: timestamp, nonce: nonce, signature: signature, }; // 4. 将JSON对象转换为字符串,然后使用自定义编码方案编码 const jsonString = JSON.stringify(allParams); const encodedData = encodeRequest(jsonString); // 5. 构建请求体(只包含编码后的data字段) const requestBody = { data: encodedData, }; // 6. 发送请求 try { const response = await fetch(`${apiBaseUrl}/api/v1/users/send-code`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); const result = await response.json(); return result; } catch (error) { console.error('发送短信失败:', error); throw error; } } // 使用示例 if (require.main === module) { console.log('=== 发送短信验证码(使用自定义编码) ==='); // 示例:发送注册验证码(使用自定义编码方案,只传递data字段) sendSMSWithEncodedSignature('13800138000', 'register', 'http://localhost:8080') .then(result => { console.log('发送成功:', result); }) .catch(error => { console.error('发送失败:', error); }); } module.exports = { sendSMSWithEncodedSignature, generateSignature, generateNonce, getSecretKey, encodeRequest, };