f
This commit is contained in:
@@ -66,6 +66,7 @@
|
|||||||
"downloadFile": true,
|
"downloadFile": true,
|
||||||
"eagerComputed": true,
|
"eagerComputed": true,
|
||||||
"effectScope": true,
|
"effectScope": true,
|
||||||
|
"encodeRequest": true,
|
||||||
"endsWith": true,
|
"endsWith": true,
|
||||||
"escape": true,
|
"escape": true,
|
||||||
"extendRef": true,
|
"extendRef": true,
|
||||||
@@ -77,6 +78,8 @@
|
|||||||
"formatMoney": true,
|
"formatMoney": true,
|
||||||
"formatPhone": true,
|
"formatPhone": true,
|
||||||
"fromNow": true,
|
"fromNow": true,
|
||||||
|
"generateNonce": true,
|
||||||
|
"generateSMSRequest": true,
|
||||||
"generateUUID": true,
|
"generateUUID": true,
|
||||||
"get": true,
|
"get": true,
|
||||||
"getActivePinia": true,
|
"getActivePinia": true,
|
||||||
|
|||||||
6
auto-imports.d.ts
vendored
6
auto-imports.d.ts
vendored
@@ -63,6 +63,7 @@ declare global {
|
|||||||
const downloadFile: typeof import('./src/utils/index.js')['downloadFile']
|
const downloadFile: typeof import('./src/utils/index.js')['downloadFile']
|
||||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const encodeRequest: typeof import('./src/utils/smsSignature.js')['encodeRequest']
|
||||||
const endsWith: typeof import('lodash-es')['endsWith']
|
const endsWith: typeof import('lodash-es')['endsWith']
|
||||||
const errorMonitor: typeof import('./src/utils/errorMonitor.js')['default']
|
const errorMonitor: typeof import('./src/utils/errorMonitor.js')['default']
|
||||||
const escape: typeof import('lodash-es')['escape']
|
const escape: typeof import('lodash-es')['escape']
|
||||||
@@ -80,6 +81,8 @@ declare global {
|
|||||||
const formatPhone: typeof import('./src/utils/index.js')['formatPhone']
|
const formatPhone: typeof import('./src/utils/index.js')['formatPhone']
|
||||||
const fromNow: typeof import('./src/utils/index.js')['fromNow']
|
const fromNow: typeof import('./src/utils/index.js')['fromNow']
|
||||||
const generateFilename: typeof import('./src/utils/export.js')['generateFilename']
|
const generateFilename: typeof import('./src/utils/export.js')['generateFilename']
|
||||||
|
const generateNonce: typeof import('./src/utils/smsSignature.js')['generateNonce']
|
||||||
|
const generateSMSRequest: typeof import('./src/utils/smsSignature.js')['generateSMSRequest']
|
||||||
const generateUUID: typeof import('./src/utils/index.js')['generateUUID']
|
const generateUUID: typeof import('./src/utils/index.js')['generateUUID']
|
||||||
const get: typeof import('lodash-es')['get']
|
const get: typeof import('lodash-es')['get']
|
||||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
@@ -512,6 +515,7 @@ declare module 'vue' {
|
|||||||
readonly downloadFile: UnwrapRef<typeof import('./src/utils/index.js')['downloadFile']>
|
readonly downloadFile: UnwrapRef<typeof import('./src/utils/index.js')['downloadFile']>
|
||||||
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||||
|
readonly encodeRequest: UnwrapRef<typeof import('./src/utils/smsSignature.js')['encodeRequest']>
|
||||||
readonly endsWith: UnwrapRef<typeof import('lodash-es')['endsWith']>
|
readonly endsWith: UnwrapRef<typeof import('lodash-es')['endsWith']>
|
||||||
readonly escape: UnwrapRef<typeof import('lodash-es')['escape']>
|
readonly escape: UnwrapRef<typeof import('lodash-es')['escape']>
|
||||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||||
@@ -523,6 +527,8 @@ declare module 'vue' {
|
|||||||
readonly formatMoney: UnwrapRef<typeof import('./src/utils/index.js')['formatMoney']>
|
readonly formatMoney: UnwrapRef<typeof import('./src/utils/index.js')['formatMoney']>
|
||||||
readonly formatPhone: UnwrapRef<typeof import('./src/utils/index.js')['formatPhone']>
|
readonly formatPhone: UnwrapRef<typeof import('./src/utils/index.js')['formatPhone']>
|
||||||
readonly fromNow: UnwrapRef<typeof import('./src/utils/index.js')['fromNow']>
|
readonly fromNow: UnwrapRef<typeof import('./src/utils/index.js')['fromNow']>
|
||||||
|
readonly generateNonce: UnwrapRef<typeof import('./src/utils/smsSignature.js')['generateNonce']>
|
||||||
|
readonly generateSMSRequest: UnwrapRef<typeof import('./src/utils/smsSignature.js')['generateSMSRequest']>
|
||||||
readonly generateUUID: UnwrapRef<typeof import('./src/utils/index.js')['generateUUID']>
|
readonly generateUUID: UnwrapRef<typeof import('./src/utils/index.js')['generateUUID']>
|
||||||
readonly get: UnwrapRef<typeof import('lodash-es')['get']>
|
readonly get: UnwrapRef<typeof import('lodash-es')['get']>
|
||||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||||
|
|||||||
266
public/examples/javascript/sms_signature_demo.js
Normal file
266
public/examples/javascript/sms_signature_demo.js
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
/**
|
||||||
|
* 短信发送接口签名示例(浏览器版本)
|
||||||
|
*
|
||||||
|
* 本示例演示如何在浏览器中为短信发送接口生成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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
246
public/examples/nodejs/sms_signature_demo.js
Normal file
246
public/examples/nodejs/sms_signature_demo.js
Normal 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,
|
||||||
|
};
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@
|
|||||||
import { userApi } from '@/api'
|
import { userApi } from '@/api'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { authEventBus } from '@/utils/request'
|
import { authEventBus } from '@/utils/request'
|
||||||
|
import { generateSMSRequest } from '@/utils/smsSignature'
|
||||||
import { clearLocalVersions, saveLocalVersions, VERSION_CONFIG, versionChecker } from '@/utils/version'
|
import { clearLocalVersions, saveLocalVersions, VERSION_CONFIG, versionChecker } from '@/utils/version'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
@@ -328,17 +329,19 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送验证码
|
// 发送验证码(使用自定义编码和签名)
|
||||||
const sendCode = async (phone, scene) => {
|
const sendCode = async (phone, scene) => {
|
||||||
try {
|
try {
|
||||||
const response = await userApi.sendCode({
|
// 1. 生成签名并编码请求数据
|
||||||
phone,
|
const encodedRequest = await generateSMSRequest(phone, scene)
|
||||||
scene
|
|
||||||
})
|
// 2. 发送编码后的请求(只包含data字段)
|
||||||
|
const response = await userApi.sendCode(encodedRequest)
|
||||||
|
|
||||||
// 后端返回格式: { success: true, data: {...}, message, ... }
|
// 后端返回格式: { success: true, data: {...}, message, ... }
|
||||||
return { success: true, data: response.data }
|
return { success: true, data: response.data }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('发送验证码失败:', error)
|
||||||
return { success: false, error }
|
return { success: false, error }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
212
src/utils/smsSignature.js
Normal file
212
src/utils/smsSignature.js
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* 短信发送接口签名和编码工具
|
||||||
|
*
|
||||||
|
* 用于生成HMAC-SHA256签名和自定义编码请求数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义编码字符集(与后端保持一致)
|
||||||
|
*/
|
||||||
|
const CUSTOM_ENCODE_CHARSET = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取签名密钥(通过多种方式混淆,增加破解难度)
|
||||||
|
* 注意:这只是示例,实际使用时应该进一步混淆
|
||||||
|
*/
|
||||||
|
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('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串(用于nonce)
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义编码请求数据
|
||||||
|
*/
|
||||||
|
export function encodeRequest(data) {
|
||||||
|
// 1. 使用自定义Base64编码
|
||||||
|
const encoded = customBase64Encode(data)
|
||||||
|
|
||||||
|
// 2. 应用字符偏移混淆(偏移7个位置)
|
||||||
|
const confused = applyCharShift(encoded, 7)
|
||||||
|
|
||||||
|
return confused
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成并编码短信发送请求数据
|
||||||
|
*
|
||||||
|
* @param {string} phone - 手机号
|
||||||
|
* @param {string} scene - 场景(register/login/change_password/reset_password等)
|
||||||
|
* @returns {Promise<{data: string}>} 编码后的请求数据
|
||||||
|
*/
|
||||||
|
export async function generateSMSRequest(phone, scene) {
|
||||||
|
// 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. 返回编码后的数据
|
||||||
|
return {
|
||||||
|
data: encodedData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user