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 次删除

查看文件

@@ -18,6 +18,11 @@ export function logout() {
return axios.post<LoginRes>('/api/user/logout');
}
/** 吊销所有后台登录态(含当前本机),所有已签发 token 立即失效 */
export function revokeAllAdminSessions() {
return axios.post<null>('/api/admin/session/revoke-all');
}
export function getUserInfo() {
return axios.post<UserState>('/api/user/info');
}

查看文件

@@ -177,6 +177,12 @@
</span>
</a-space>
</a-doption>
<a-doption>
<a-space @click="handleRevokeAll">
<icon-lock />
<span>吊销所有后台登录态</span>
</a-space>
</a-doption>
<a-doption>
<a-space @click="handleLogout">
<icon-export />
@@ -200,6 +206,7 @@
import { LOCALE_OPTIONS } from '@/locale';
import useLocale from '@/hooks/locale';
import useUser from '@/hooks/user';
import { revokeAllAdminSessions } from '@/api/user';
import Menu from '@/components/menu/index.vue';
import MessageBox from '../message-box/index.vue';
@@ -247,6 +254,17 @@
const handleLogout = () => {
logout();
};
const handleRevokeAll = async () => {
try {
await revokeAllAdminSessions();
Message.success('已吊销所有后台登录态(含本机),请重新登录');
setTimeout(() => {
logout();
}, 800);
} catch (e: any) {
Message.error(e?.message || '吊销失败,请重试');
}
};
const setDropDownVisible = () => {
const event = new MouseEvent('click', {
view: window,

查看文件

@@ -18,6 +18,9 @@ USER_TOKEN_SECRET=<另一个强随机长串>
ADMIN_USERNAME=<后台账号,默认 admin>
ADMIN_PASSWORD=<强口令;多个可用 | 分隔>
# 可选:后台令牌有效期小时数(默认 24;改口令/被盗时可在后台右上角「吊销所有后台登录态」立即失效全部令牌)
# ADMIN_TOKEN_TTL_HOURS=24
# 数据库
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306

查看文件

@@ -26,13 +26,11 @@
"@types/multer": "^2.3.0",
"@types/node": "^22.13.4",
"jest": "^29.7.0",
"prisma": "8.0.0-rc.17",
"ts-jest": "^29.2.5",
"tsx": "^4.23.15",
"typescript": "^5.7.3"
},
"dependencies": {
"@prisma/client": "^7.10.0",
"cors": "^2.8.6",
"dotenv": "^18.0.4",
"express": "^5.2.1",

3924
server/pnpm-lock.yaml 自动生成的

文件差异内容过多而无法显示 加载差异

查看文件

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

查看文件

@@ -0,0 +1,40 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第七十五批回归闸门:后台登录态吊销 + 依赖清理。
*/
describe('第七十五批后台令牌吊销闸门', () => {
const root = path.resolve(__dirname, '../../');
const indexCode = fs.readFileSync(path.join(root, 'server/src/index.ts'), 'utf-8');
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'server/package.json'), 'utf-8'));
const navbar = fs.readFileSync(path.join(root, 'admin/arco-design-pro-vite/src/components/navbar/index.vue'), 'utf-8');
it('令牌携带版本号,版本不符即失效', () => {
expect(indexCode).toContain('ver: adminTokenEpoch');
expect(indexCode).toContain('if (Number(data.ver) !== adminTokenEpoch) return false;');
expect(indexCode).toContain('let adminTokenEpoch = 1;');
});
it('提供吊销接口并持久化版本号', () => {
expect(indexCode).toContain("app.post('/api/admin/session/revoke-all', requireAdmin");
expect(indexCode).toContain("repo.updatePlatformConfig('admin_token_epoch'");
expect(indexCode).toContain('loadAdminTokenEpoch');
});
it('令牌有效期可配置(默认 24 小时)', () => {
expect(indexCode).toContain('ADMIN_TOKEN_TTL_HOURS');
expect(indexCode).toContain('ADMIN_TOKEN_TTL_MS');
});
it('后台提供「吊销所有后台登录态」入口', () => {
expect(navbar).toContain('handleRevokeAll');
expect(navbar).toContain('revokeAllAdminSessions');
});
it('已移除未使用的 prisma 依赖(消除全部依赖漏洞)', () => {
expect(pkg.dependencies['@prisma/client']).toBeUndefined();
expect(pkg.devDependencies['prisma']).toBeUndefined();
});
});