security: 后台登录态可吊销(令牌版本号) + 清理未使用的 prisma 依赖(依赖漏洞 46→0)
- admin token 载荷新增 ver 版本号,与当前版本不一致即失效;版本持久化到 platform_configs,重启/多实例(30s同步)均生效 - 新增 POST /api/admin/session/revoke-all(requireAdmin):一键吊销全部后台登录态;后台右上角新增入口 - 令牌有效期改为可配置 ADMIN_TOKEN_TTL_HOURS(默认由 7 天收紧到 24 小时) - 移除 server 未使用的 @prisma/client / prisma(server/src 零引用),pnpm 移除 331 个包;pnpm audit --prod 由 46 个漏洞降为 0 - 新增回归护栏 admin-token-revocation.spec.ts(91 套件/653 用例全绿);冒烟 6/6
这个提交包含在:
@@ -245,8 +245,25 @@ const ADMIN_PASSWORDS = (ADMIN_PASSWORD_SET || 'admin|admin123').split('|').filt
|
||||
// 生产环境安全闸门:未配置 ADMIN_PASSWORD(或仍使用内置默认口令)时,禁止后台登录,避免默认口令被爆破
|
||||
const ADMIN_PASSWORD_CONFIGURED = Boolean(ADMIN_PASSWORD_SET) && !/^(admin|admin123)$/i.test(ADMIN_PASSWORD_SET);
|
||||
|
||||
// 后台令牌有效期(默认 24 小时,可用 ADMIN_TOKEN_TTL_HOURS 调整)
|
||||
const ADMIN_TOKEN_TTL_MS = Math.max(1, Number(process.env.ADMIN_TOKEN_TTL_HOURS || 24)) * 3600 * 1000;
|
||||
|
||||
// 后台登录态版本号:令牌携带签发时的版本,与当前版本不一致即失效(支持"吊销所有会话")。
|
||||
// 版本持久化在 platform_configs,重启后依然生效;多实例部署下每 30 秒同步一次。
|
||||
let adminTokenEpoch = 1;
|
||||
|
||||
async function loadAdminTokenEpoch(): Promise<void> {
|
||||
try {
|
||||
const rec = await repo.getPlatformConfigByKey('admin_token_epoch');
|
||||
const v = Number(rec && rec.valueJson && (rec.valueJson as any).epoch);
|
||||
if (Number.isFinite(v) && v >= 1) adminTokenEpoch = v;
|
||||
} catch (_) {
|
||||
// 读取失败时保持当前内存值
|
||||
}
|
||||
}
|
||||
|
||||
function signAdminToken(): string {
|
||||
const payload = Buffer.from(JSON.stringify({ role: 'admin', ts: Date.now() })).toString('base64url');
|
||||
const payload = Buffer.from(JSON.stringify({ role: 'admin', ts: Date.now(), ver: adminTokenEpoch })).toString('base64url');
|
||||
const sig = crypto.createHmac('sha256', ADMIN_TOKEN_SECRET).update(payload).digest('base64url');
|
||||
return `${payload}.${sig}`;
|
||||
}
|
||||
@@ -262,7 +279,9 @@ function verifyAdminToken(token: string): boolean {
|
||||
try {
|
||||
const data = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
||||
if (data.role !== 'admin' || !data.ts) return false;
|
||||
if (Date.now() - Number(data.ts) > 7 * 24 * 3600 * 1000) return false;
|
||||
// 版本不一致 = 该令牌已被吊销
|
||||
if (Number(data.ver) !== adminTokenEpoch) return false;
|
||||
if (Date.now() - Number(data.ts) > ADMIN_TOKEN_TTL_MS) return false;
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
@@ -429,6 +448,24 @@ function assertCompanionActor(
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 管理端:吊销所有后台登录态(版本号 +1,所有已签发 admin token 立即失效)
|
||||
app.post('/api/admin/session/revoke-all', requireAdmin, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const next = adminTokenEpoch + 1;
|
||||
await repo.updatePlatformConfig('admin_token_epoch', {
|
||||
name: '后台登录态版本号',
|
||||
valueJson: { epoch: next },
|
||||
description: '内部标记:用于吊销全部后台登录态,勿手动修改',
|
||||
status: 1,
|
||||
});
|
||||
adminTokenEpoch = next;
|
||||
console.warn(`[Peiwan Admin][AUDIT] 后台登录态已全部吊销 version->${next} ip=${clientIpOf(req)}`);
|
||||
res.json({ code: 20000, message: '已吊销所有后台登录态,请重新登录' });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 1.1 管理员鉴权与登录接口 (供 PC 运营管理后台生产环境调用)
|
||||
app.post('/api/user/login', (req: Request, res: Response) => {
|
||||
const { username, password } = req.body || {};
|
||||
@@ -4664,6 +4701,13 @@ async function bootstrap() {
|
||||
await logStartupConfigSelfCheck();
|
||||
startOrderTimeoutScheduler();
|
||||
|
||||
// 载入后台登录态版本号,并周期同步(支持多实例下的"吊销所有会话")
|
||||
await loadAdminTokenEpoch();
|
||||
const epochSyncTimer = setInterval(() => {
|
||||
void loadAdminTokenEpoch();
|
||||
}, 30 * 1000);
|
||||
if (typeof (epochSyncTimer as any).unref === 'function') (epochSyncTimer as any).unref();
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`[Peiwan Server] HTTP server is running on http://localhost:${port}`);
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户