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
这个提交包含在:
xcbyy
2026-10-19 14:08:58 +08:00
父节点 0752acda40
当前提交 12bbf4fc6b
共有 7 个文件被更改,包括 114 次插入 和 3926 次删除

查看文件

@@ -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}`);
});