f
This commit is contained in:
11
public/examples/nodejs/.eslintignore
Normal file
11
public/examples/nodejs/.eslintignore
Normal file
@@ -0,0 +1,11 @@
|
||||
# ESLint 忽略配置 - 忽略示例代码目录下的所有语法错误
|
||||
# 这些是示例代码文件,不需要进行 ESLint 语法检查
|
||||
|
||||
# 忽略当前目录下的所有 .js 文件
|
||||
*.js
|
||||
|
||||
# 忽略当前目录下的所有文件
|
||||
*
|
||||
|
||||
# 忽略子目录
|
||||
*/
|
||||
131
public/examples/nodejs/demo.js
Normal file
131
public/examples/nodejs/demo.js
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const crypto = require('crypto')
|
||||
|
||||
// ==================== API配置 ====================
|
||||
const ACCESS_ID = 'XXXXXXXXX' // 替换为您的ACCESS_ID
|
||||
const APP_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXX' // 替换为您的app_secret
|
||||
const BASE_URL = 'https://api.haiyudata.com'
|
||||
|
||||
// ==================== 测试参数 ====================
|
||||
const API_CODE = 'FLXG0V4B' // 替换为您的API编号
|
||||
|
||||
const PARAMS = {
|
||||
name: 'XXXX',
|
||||
id_card: 'XXXXXXXXXXXXXXX',
|
||||
auth_date: '20250722-20250923',
|
||||
}
|
||||
|
||||
// ==================== 加密解密函数 ====================
|
||||
function encrypt_data(data) {
|
||||
const key = Buffer.from(APP_SECRET, 'hex')
|
||||
const iv = crypto.randomBytes(16)
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv)
|
||||
cipher.setAutoPadding(true)
|
||||
let encrypted = cipher.update(data, 'utf8')
|
||||
encrypted = Buffer.concat([iv, encrypted, cipher.final()])
|
||||
return encrypted.toString('base64')
|
||||
}
|
||||
|
||||
function decrypt_data(encrypted_data) {
|
||||
const key = Buffer.from(APP_SECRET, 'hex')
|
||||
const encryptedBuffer = Buffer.from(encrypted_data, 'base64')
|
||||
const iv = encryptedBuffer.slice(0, 16)
|
||||
const ciphertext = encryptedBuffer.slice(16)
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
|
||||
decipher.setAutoPadding(true)
|
||||
let decrypted = decipher.update(ciphertext)
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()])
|
||||
return decrypted.toString('utf8')
|
||||
}
|
||||
|
||||
// ==================== 主测试函数 ====================
|
||||
async function test_api() {
|
||||
try {
|
||||
console.log(`=== 测试API: ${API_CODE} ===`)
|
||||
console.log(`请求参数: ${JSON.stringify(PARAMS, null, 2)}`)
|
||||
|
||||
// 加密参数
|
||||
const params_json = JSON.stringify(PARAMS)
|
||||
const encrypted_data = encrypt_data(params_json)
|
||||
|
||||
// 构建请求
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Id': ACCESS_ID,
|
||||
}
|
||||
|
||||
const url = `${BASE_URL}/api/v1/${API_CODE}`
|
||||
const request_data = { data: encrypted_data, options: { json: true } }
|
||||
|
||||
console.log(`请求URL: ${url}`)
|
||||
console.log(`请求头: ${JSON.stringify(headers, null, 2)}`)
|
||||
|
||||
// 发送请求
|
||||
const start_time = Date.now()
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(request_data),
|
||||
signal: AbortSignal.timeout(30000), // 30秒超时
|
||||
})
|
||||
const elapsed_time = Date.now() - start_time
|
||||
|
||||
console.log(`\n=== 响应信息 ===`)
|
||||
console.log(`状态码: ${response.status}`)
|
||||
console.log(`耗时: ${elapsed_time}ms`)
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log(`请求失败: ${response.status} ${response.statusText}`)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
try {
|
||||
const response_json = await response.json()
|
||||
console.log(`原始响应: ${JSON.stringify(response_json, null, 2)}`)
|
||||
|
||||
// 检查响应格式
|
||||
if (!('code' in response_json)) {
|
||||
console.log('直接返回业务数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 标准格式处理
|
||||
const api_code = response_json.code
|
||||
const api_message = response_json.message || ''
|
||||
const encrypted_response = response_json.data || ''
|
||||
|
||||
console.log(`API响应码: ${api_code}`)
|
||||
console.log(`API消息: ${api_message}`)
|
||||
|
||||
if (api_code !== 0) {
|
||||
console.log(`API错误: ${api_message}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!encrypted_response) {
|
||||
console.log('无加密数据返回')
|
||||
return
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
try {
|
||||
const decrypted_data = decrypt_data(encrypted_response)
|
||||
const result_data = JSON.parse(decrypted_data)
|
||||
|
||||
console.log(`\n=== 解密后的数据 ===`)
|
||||
console.log(JSON.stringify(result_data, null, 2))
|
||||
} catch (e) {
|
||||
console.log(`解密失败: ${e.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`响应不是JSON格式: ${e.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`测试异常: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
test_api()
|
||||
287
public/examples/nodejs/sms_signature_demo.js
Normal file
287
public/examples/nodejs/sms_signature_demo.js
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 短信发送接口签名示例
|
||||
*
|
||||
* 本示例演示如何为短信发送接口生成HMAC-SHA256签名
|
||||
*
|
||||
* 安全提示:
|
||||
* 1. 密钥应该通过代码混淆、字符串拆分等方式隐藏
|
||||
* 2. 不要在前端代码中直接暴露完整密钥
|
||||
* 3. 建议使用构建工具进行代码混淆
|
||||
*/
|
||||
|
||||
const crypto = require('crypto')
|
||||
|
||||
/**
|
||||
* 获取签名密钥(通过多种方式混淆,增加破解难度)
|
||||
* 注意:这只是示例,实际使用时应该进一步混淆
|
||||
*/
|
||||
function getSecretKey() {
|
||||
// 方式1: 字符串拆分和拼接
|
||||
const part1 = 'HyApi2026'
|
||||
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('')
|
||||
|
||||
// 组合多种方式(实际密钥:HyApi2026SMSSecretKey!@#$%^&*()_+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,
|
||||
}
|
||||
Reference in New Issue
Block a user