/** * 设备登录接口测试脚本 * * 复现服务端的加密与签名逻辑: * - AES-CBC 加密: key=SHA256(secret)[:32], iv=SHA256(md5hex(nonce)+secret)[:16] * - 签名: HMAC-SHA256(appSecret, METHOD\nPATH\nQUERY\nBODY_SHA256\nAppId\nTimestamp\nNonce) */ import crypto from 'node:crypto'; import https from 'node:https'; import http from 'node:http'; // ─── 配置(对应 api-dev.yaml)───────────────────────────────────────────────── const CONFIG = { baseUrl: 'http://127.0.0.1:8080', // 本地测试改这里;生产改为 https://tapi.hifast.biz appId: 'android-client', appSecret: 'uB4G,XxL2{7b}', // AppSignature.AppSecrets[appId] securitySecret: 'uB4G,XxL2{7b', // Security.SecuritySecret(注意少一个 }) }; // ─── AES-CBC 加密工具(与 pkg/cryptox/aes.go 对齐)───────────────────────────── function generateKey(secret) { return crypto.createHash('sha256').update(secret).digest().slice(0, 32); } function generateIv(nonce, secret) { const md5hex = crypto.createHash('md5').update(nonce).digest('hex'); return crypto.createHash('sha256').update(md5hex + secret).digest().slice(0, 16); } function aesEncrypt(plaintext, secret) { const nonce = crypto.randomBytes(8).toString('hex'); // 16位 hex,对应 Go 的 fmt.Sprintf("%x", UnixNano) const key = generateKey(secret); const iv = generateIv(nonce, secret); const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); return { data: encrypted.toString('base64'), time: nonce, }; } // ─── 签名工具(与 pkg/signature/canonical.go 对齐)──────────────────────────── function sha256Hex(data) { return crypto.createHash('sha256').update(data).digest('hex'); } function buildStringToSign(method, path, rawQuery, bodyBytes, appId, timestamp, nonce) { // 规范化 query(按 key 字母排序) let canonical = ''; if (rawQuery) { const params = new URLSearchParams(rawQuery); const sorted = [...params.entries()].sort((a, b) => a[0].localeCompare(b[0])); canonical = sorted.map(([k, v]) => `${k}=${v}`).join('&'); } const bodyHash = sha256Hex(bodyBytes); return [method.toUpperCase(), path, canonical, bodyHash, appId, timestamp, nonce].join('\n'); } function hmacSha256(secret, data) { return crypto.createHmac('sha256', secret).update(data).digest('hex'); } // ─── 发送请求 ───────────────────────────────────────────────────────────────── async function request(method, path, payload) { const url = new URL(CONFIG.baseUrl + path); const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = crypto.randomBytes(16).toString('hex'); // 1. AES 加密 body const { data: encData, time: encTime } = aesEncrypt(JSON.stringify(payload), CONFIG.securitySecret); const bodyObj = { data: encData, time: encTime }; const bodyStr = JSON.stringify(bodyObj); const bodyBytes = Buffer.from(bodyStr, 'utf8'); // 2. 构造签名 const sts = buildStringToSign(method, url.pathname, url.search.slice(1), bodyBytes, CONFIG.appId, timestamp, nonce); const sig = hmacSha256(CONFIG.appSecret, sts); const headers = { 'Content-Type': 'application/json', 'X-App-Id': CONFIG.appId, 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'X-Signature': sig, 'Login-Type': 'device', }; console.log('\n📤 请求信息'); console.log(' URL:', url.toString()); console.log(' timestamp:', timestamp); console.log(' nonce:', nonce); console.log(' signature:', sig); console.log(' 加密 nonce(time):', encTime); console.log(' 签名原文:\n' + sts.split('\n').map(l => ' ' + l).join('\n')); console.log(' body:', bodyStr); return new Promise((resolve, reject) => { const lib = url.protocol === 'https:' ? https : http; const req = lib.request(url, { method, headers }, (res) => { let raw = ''; res.on('data', chunk => raw += chunk); res.on('end', () => { console.log('\n📥 响应'); console.log(' status:', res.statusCode); try { console.log(' body:', JSON.stringify(JSON.parse(raw), null, 2)); } catch { console.log(' body:', raw); } resolve({ status: res.statusCode, body: raw }); }); }); req.on('error', reject); req.write(bodyStr); req.end(); }); } // ─── 测试用例 ────────────────────────────────────────────────────────────────── async function main() { console.log('='.repeat(60)); console.log('🔐 设备登录接口测试'); console.log('='.repeat(60)); // 测试 1:正常设备登录 console.log('\n【TEST 1】正常设备登录'); await request('POST', '/api/v1/auth/login/device', { identifier: 'test-device-001', user_agent: 'TestClient/1.0 (Node.js test)', short_code: '', cf_token: '', }); // 测试 2:不同 identifier(新设备,应该自动注册) console.log('\n【TEST 2】新设备首次登录'); await request('POST', '/api/v1/auth/login/device', { identifier: `device-${Date.now()}`, user_agent: 'TestClient/1.0 (Node.js test)', }); } main().catch(console.error);