This commit is contained in:
2026-02-12 13:27:11 +08:00
parent 9ce64757fc
commit 34d3e4c715
6 changed files with 741 additions and 5 deletions

View File

@@ -0,0 +1,246 @@
/**
* 短信发送接口签名示例
*
* 本示例演示如何为短信发送接口生成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<Object>} 响应结果
*/
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,
};