Files
tyapi-frontend/public/examples/nodejs/sms_signature_demo.js
2026-02-12 13:27:11 +08:00

247 lines
7.1 KiB
JavaScript
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. 密钥应该通过代码混淆、字符串拆分等方式隐藏
* 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,
};