zero-ppanel/test_device_login.mjs
shanshanzhong b8dab70de5
Some checks failed
Build docker and publish / prepare (20.15.1) (push) Successful in 10s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.admin image_name:ppanel-admin name:admin]) (push) Successful in 1m16s
Build docker and publish / deploy (push) Has been cancelled
Build docker and publish / notify (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.rpc-core image_name:ppanel-rpc-core name:rpc-core]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.scheduler image_name:ppanel-scheduler name:scheduler]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.node image_name:ppanel-node name:node]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.queue image_name:ppanel-queue name:queue]) (push) Has been cancelled
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.api image_name:ppanel-api name:api]) (push) Has been cancelled
feat: 新增设备登录开关配置,优化 CI Go 模块缓存机制,并添加解密中间件调试日志。
2026-03-01 20:21:02 -08:00

149 lines
5.6 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.

/**
* 设备登录接口测试脚本
*
* 复现服务端的加密与签名逻辑:
* - 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);