feat: 新增设备登录开关配置,优化 CI Go 模块缓存机制,并添加解密中间件调试日志。
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

This commit is contained in:
2026-03-01 20:21:02 -08:00
parent d3da56891b
commit b8dab70de5
7 changed files with 171 additions and 10 deletions
+148
View File
@@ -0,0 +1,148 @@
/**
* 设备登录接口测试脚本
*
* 复现服务端的加密与签名逻辑:
* - 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);