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

267 lines
7.8 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. 建议使用构建工具进行代码混淆和压缩
* 4. 可以考虑将签名逻辑放在后端代理接口中
*/
/**
* 获取签名密钥(通过多种方式混淆,增加破解难度)
* 注意:这只是示例,实际使用时应该进一步混淆
*/
function getSecretKey() {
// 方式1: 字符串拆分和拼接
const part1 = 'TyApi2024';
const part2 = 'SMSSecret';
const part3 = 'Key!@#$%^';
const part4 = '&*()_+QWERTY';
const part5 = 'UIOP';
// 方式2: 使用数组和join增加混淆
const arr = [part1, part2, part3, part4, part5];
return arr.join('');
// 方式3: 字符数组拼接(更复杂的方式)
// const chars = ['T', 'y', 'A', 'p', 'i', '2', '0', '2', '4', ...];
// return chars.join('');
// 方式4: 使用atob解码如果密钥经过base64编码
// const encoded = 'base64_encoded_string';
// return atob(encoded);
}
/**
* 生成随机字符串用于nonce
*/
function generateNonce(length = 16) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
const array = new Uint8Array(length);
crypto.getRandomValues(array);
for (let i = 0; i < length; i++) {
result += chars[array[i] % chars.length];
}
return result;
}
/**
* 使用Web Crypto API生成HMAC-SHA256签名
*
* @param {Object} params - 请求参数对象
* @param {string} secretKey - 签名密钥
* @param {number} timestamp - 时间戳(秒)
* @param {string} nonce - 随机字符串
* @returns {Promise<string>} 签名字符串hex编码
*/
async 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. 使用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);
// 转换为hex字符串
const hashArray = Array.from(new Uint8Array(signature));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
/**
* 自定义编码字符集(与后端保持一致)
*/
const CUSTOM_ENCODE_CHARSET = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?";
/**
* 自定义Base64编码使用自定义字符集
*/
function customBase64Encode(data) {
if (data.length === 0) return '';
// 将字符串转换为UTF-8字节数组
const encoder = new TextEncoder();
const bytes = encoder.encode(data);
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 = await 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 (typeof window !== 'undefined') {
window.SMSSignature = {
sendSMSWithEncodedSignature,
generateSignature,
generateNonce,
encodeRequest,
};
}
// 如果在Node.js环境中使用需要安装crypto-js等库
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
sendSMSWithEncodedSignature,
generateSignature,
generateNonce,
getSecretKey,
encodeRequest,
};
}