feat: harden sessions and track downstream acknowledgements

This commit is contained in:
hectorzhao
2026-07-14 14:18:43 +08:00
parent 3d37adcc9f
commit 8c03663f24
43 changed files with 1733 additions and 150 deletions
+52 -9
View File
@@ -1,33 +1,76 @@
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuthSessionRecord, DEVELOPMENT_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, SessionService } from './session.service';
export type SessionRequest = {
header(name: string): string | undefined;
originalUrl?: string;
url?: string;
sessionUserId?: string;
sessionToken?: string;
authSession?: AuthSessionRecord;
};
@Injectable()
export class SessionValidationMiddleware implements NestMiddleware {
constructor(private readonly prisma: PrismaService) {}
constructor(private readonly prisma: PrismaService, private readonly sessions: SessionService) {}
async use(request: SessionRequest, _: unknown, next: () => void) {
const authorization = request.header('authorization');
if (!authorization) {
const path = request.originalUrl ?? request.url ?? '';
if (/\/(admin|client)\/auth\/(captcha|login)(?:\?|$)/.test(path)) {
next();
return;
}
const match = /^Bearer dev-token:([^:]+):(\d+)$/.exec(authorization.trim());
if (!match) {
throw new UnauthorizedException('登录会话无效,请重新登录');
const token = this.readCookie(request.header('cookie'));
if (!token) {
if ((path.includes('/admin/') && !path.includes('/admin/gateway/')) || path.includes('/client/') || path.includes('/auth/')) {
throw this.unauthorized('SESSION_INVALID', '请先登录');
}
next();
return;
}
const result = await this.sessions.validate(token, request.header('x-session-activity') === 'user');
if (result.status === 'expired') {
throw this.unauthorized(result.code, result.code === 'SESSION_ABSOLUTE_TIMEOUT' ? '登录已满 12 小时,请重新登录' : '登录会话已失效,请重新登录');
}
const user = await this.prisma.user.findUnique({
where: { id: match[1] },
where: { id: result.record.userId },
select: { id: true, status: true, deletedAt: true, sessionVersion: true },
});
if (!user || user.status !== 'active' || user.deletedAt || user.sessionVersion !== Number(match[2])) {
throw new UnauthorizedException('登录会话已失效,请重新登录');
if (!user || user.status !== 'active' || user.deletedAt || user.sessionVersion !== result.record.sessionVersion) {
await this.sessions.remove(token);
throw this.unauthorized('SESSION_REVOKED', '登录会话已被撤销,请重新登录');
}
if ((path.includes('/admin/') && result.record.portal !== 'admin') || (path.includes('/client/') && result.record.portal !== 'client')) {
throw this.unauthorized('SESSION_PORTAL_MISMATCH', '登录入口与当前会话不匹配');
}
request.sessionUserId = user.id;
request.sessionToken = token;
request.authSession = result.record;
if (result.status === 'locked' && !path.includes('/auth/session/unlock') && !path.includes('/auth/logout')) {
if (result.newlyLocked) {
await this.prisma.operationLog.create({
data: { userId: user.id, action: 'auth.session_locked', resource: 'auth_session', detail: { portal: result.record.portal, reason: 'idle_timeout' } },
});
}
throw this.unauthorized('SESSION_LOCKED', '由于长时间未操作,会话已安全锁定');
}
next();
}
private readCookie(cookieHeader?: string) {
if (!cookieHeader) return undefined;
for (const part of cookieHeader.split(';')) {
const [name, ...value] = part.trim().split('=');
if (name === SESSION_COOKIE_NAME || name === DEVELOPMENT_SESSION_COOKIE_NAME) return decodeURIComponent(value.join('='));
}
return undefined;
}
private unauthorized(code: string, message: string) {
return new UnauthorizedException({ code, message });
}
}