feat: 记录登录审计上下文

- src/auth/auth.controller.ts: 登录时提取 requestId、来源 IP 和 user-agent 传入服务层
- src/auth/auth.service.ts: 登录成功审计补齐请求上下文,登录失败写入 USER_LOGIN_FAILED
- src/auth/auth.types.ts: 补充登录审计请求上下文类型
This commit is contained in:
湛兮
2026-06-12 06:10:16 +08:00
parent b305922a5b
commit 581e14cf30
3 changed files with 76 additions and 4 deletions
+23 -3
View File
@@ -1,4 +1,7 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
/**
* 登录鉴权 HTTP 入口,负责校验入参并把请求上下文传给审计链路。
*/
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import {
ApiBody,
ApiCreatedResponse,
@@ -20,7 +23,13 @@ import {
} from './auth.dto';
import { AuthService } from './auth.service';
import { CurrentUser } from './current-user.decorator';
import { AuthenticatedUser, LoginResult } from './auth.types';
import {
AuthenticatedUser,
AuthRequestContext,
LoginResult,
RequestWithUser,
} from './auth.types';
import { resolveRequestId } from '../common/http/request-id';
@ApiTags('登录鉴权')
@Controller('auth')
@@ -31,9 +40,10 @@ export class AuthController {
@ApiBody({ type: LoginDto })
@ApiCreatedResponse({ description: '登录成功,返回登录令牌和当前账号信息。' })
async login(
@Req() request: RequestWithUser,
@Body(new ZodValidationPipe(loginSchema)) body: LoginInput,
): Promise<LoginResult> {
return this.authService.login(body);
return this.authService.login(body, this.auditContextFor(request));
}
@Get('me')
@@ -75,4 +85,14 @@ export class AuthController {
message: '已退出登录',
};
}
private auditContextFor(request: RequestWithUser): AuthRequestContext {
const userAgent = request.headers['user-agent'];
return {
requestId: resolveRequestId(request.headers),
sourceIp: request.ip ?? request.socket?.remoteAddress,
userAgent: Array.isArray(userAgent) ? userAgent[0] : userAgent,
};
}
}
+40 -1
View File
@@ -1,3 +1,6 @@
/**
* 登录、密码和项目权限服务,统一维护账号状态与鉴权审计。
*/
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuditService } from '../audit/audit.service';
@@ -15,6 +18,7 @@ import {
LoginInput,
} from './auth.dto';
import {
AuthRequestContext,
AuthenticatedUser,
LoginResult,
ProjectPermissionLevel,
@@ -41,14 +45,19 @@ export class AuthService implements OnModuleInit {
await this.userRepository.ensureSuperAdmin(passwordHash);
}
async login(input: LoginInput): Promise<LoginResult> {
async login(
input: LoginInput,
context: AuthRequestContext = {},
): Promise<LoginResult> {
const user = await this.userRepository.findByAccount(input.account);
if (!user) {
await this.recordLoginFailure(input.account, 'account_not_found', context);
throw new AppError('AUTH_REQUIRED', '账号或密码错误', 401);
}
if (user.status !== 'active') {
await this.recordLoginFailure(input.account, 'disabled', context, user);
throw new AppError('FORBIDDEN', '账号已被禁用,请联系超级管理员', 403);
}
@@ -58,6 +67,7 @@ export class AuthService implements OnModuleInit {
);
if (!passwordMatched) {
await this.recordLoginFailure(input.account, 'invalid_password', context, user);
throw new AppError('AUTH_REQUIRED', '账号或密码错误', 401);
}
@@ -68,6 +78,9 @@ export class AuthService implements OnModuleInit {
resourceId: user.id,
actorId: user.id,
actorName: user.account,
requestId: context.requestId,
sourceIp: context.sourceIp,
userAgent: context.userAgent,
after: {
role: user.role,
mustChangePassword: user.mustChangePassword,
@@ -230,6 +243,32 @@ export class AuthService implements OnModuleInit {
return user;
}
private async recordLoginFailure(
account: string,
reason: 'account_not_found' | 'disabled' | 'invalid_password',
context: AuthRequestContext,
user?: UserRecord,
): Promise<void> {
await this.auditService.record({
action: 'USER_LOGIN_FAILED',
resourceType: 'user',
resourceId: user?.id,
actorId: user?.id,
actorName: user?.account ?? account.trim(),
requestId: context.requestId,
sourceIp: context.sourceIp,
userAgent: context.userAgent,
after: {
reason,
role: user?.role,
status: user?.status,
},
parameterDigest: {
account: account.trim(),
},
});
}
private resolveSuperAdminInitialPassword(): string {
const password = this.config.get('SUPER_ADMIN_INITIAL_PASSWORD', {
infer: true,
+13
View File
@@ -1,3 +1,6 @@
/**
* 登录鉴权模块的账号、权限和请求上下文类型。
*/
export type AuthRole = 'super_admin' | 'member';
export type AuthStatus = 'active' | 'disabled';
@@ -51,7 +54,17 @@ export type LoginResult = {
expiresAt: string;
};
export type AuthRequestContext = {
requestId?: string;
sourceIp?: string;
userAgent?: string;
};
export type RequestWithUser = {
headers: Record<string, string | string[] | undefined>;
ip?: string;
socket?: {
remoteAddress?: string;
};
user?: AuthenticatedUser;
};