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

This commit is contained in:
shanshanzhong 2026-03-01 20:21:02 -08:00
parent d3da56891b
commit b8dab70de5
7 changed files with 171 additions and 10 deletions

View File

@ -172,6 +172,9 @@ jobs:
runs-on: zero-ppanel-server
container: # <-- 整个 build job 在 Node.js 容器中运行
image: node:20.15.1
volumes:
- /root/go/pkg/mod:/root/go/pkg/mod
- /root/.cache/go-build:/root/.cache/go-build
needs: prepare
if: needs.prepare.outputs.has_changes == 'true' && contains(needs.prepare.outputs.changed_services, matrix.service.name)
strategy:
@ -206,16 +209,6 @@ jobs:
with:
go-version: '1.24.0' # 确保使用 go.mod 中指定的精确版本
- name: 🗄️ 缓存 Go modules
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: 🔧 确保 Docker CLI 可用并初始化 Go Modules
run: |
set -e

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ logs/
etc/*.local.yaml
apps/*/etc/*.local.yaml
deploy/.env
goctl_tpl/
# IDE
.idea/

View File

@ -49,6 +49,9 @@ Security:
Enable: true
SecuritySecret: "uB4G,XxL2{7b"
Device:
Enable: true
Asynq:
Addr: 127.0.0.1:6379

View File

@ -23,4 +23,7 @@ type Config struct {
Enable bool
SecuritySecret string
}
Device struct {
Enable bool
}
}

View File

@ -34,6 +34,11 @@ func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Devic
}
func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginReq) (resp *types.LoginResp, err error) {
// todo: 新建设备登录是否禁用逻辑
if !l.svcCtx.Config.Device.Enable {
return nil, xerr.NewErrCode(xerr.DeviceLoginDisabled)
}
// 1. 调用 Core RPC 设备登录
rpcResp, err := l.svcCtx.CoreRpc.DeviceLogin(l.ctx, &core.DeviceLoginReq{
Identifier: req.Identifier,

View File

@ -8,6 +8,7 @@ import (
"io"
"net"
"net/http"
"net/url"
"strings"
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/config"
@ -26,6 +27,8 @@ func NewDecryptMiddleware(c config.Config) *DecryptMiddleware {
func (m *DecryptMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[DEBUG] DecryptMiddleware entered, Security.Enable=%v, Login-Type=%q\n",
m.conf.Security.Enable, r.Header.Get("Login-Type"))
if !m.conf.Security.Enable {
next(w, r)
return
@ -86,7 +89,12 @@ func (m *DecryptMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
httpx.Error(w, xerr.NewErrCode(xerr.DecryptFailed))
return
}
fmt.Printf("[DEBUG] decrypted body: %s\n", string(plain))
r.Body = io.NopCloser(bytes.NewBuffer(plain))
// 防止 httpx.Parse 内部的 r.ParseForm() 消费已替换的 body
// ParseForm 首行检查 r.PostForm == nil置空后它不会再读 body。
r.PostForm = url.Values{}
r.ContentLength = int64(len(plain))
}
next(rw, r)

148
test_device_login.mjs Normal file
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);