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
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:
@@ -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);
|
||||
Reference in New Issue
Block a user