diff --git a/src/app.module.ts b/src/app.module.ts index c338b10..6cc41ed 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { AuthModule } from './auth/auth.module.js'; import { JwtAuthGuard } from './auth/jwt-auth.guard.js'; import { PermissionsGuard } from './auth/permissions.guard.js'; import { RequestContextInterceptor } from './auth/request-context.interceptor.js'; +import { BodyLoggingInterceptor } from './logging/body-logging.interceptor.js'; import { TransformInterceptor } from './common/interceptors/transform.interceptor.js'; import { AllExceptionsFilter } from './common/filters/http-exception.filter.js'; import { TenantsModule } from './tenants/tenants.module.js'; @@ -41,6 +42,7 @@ import { AppService } from './app.service.js'; { provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: PermissionsGuard }, { provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor }, + { provide: APP_INTERCEPTOR, useClass: BodyLoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, { provide: APP_FILTER, useClass: AllExceptionsFilter }, ], diff --git a/src/logging/body-logging.interceptor.ts b/src/logging/body-logging.interceptor.ts new file mode 100644 index 0000000..469a7c4 --- /dev/null +++ b/src/logging/body-logging.interceptor.ts @@ -0,0 +1,44 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; + +const SENSITIVE_KEYS = new Set([ + 'password', + 'passwordhash', + 'oldpassword', + 'newpassword', + 'confirmpassword', + 'refreshtoken', + 'token', + 'secret', + 'authorization', +]); + +/** 递归脱敏:命中的字段值替换为 ***。 */ +function mask(value: unknown): unknown { + if (value === null || value === undefined) return value; + if (Array.isArray(value)) return value.map(mask); + if (typeof value === 'object') { + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + out[k] = SENSITIVE_KEYS.has(k.toLowerCase()) ? '***' : mask(v); + } + return out; + } + return value; +} + +/** 记录请求 body(POST/PUT/PATCH/DELETE),敏感字段脱敏,级别 debug。 */ +@Injectable() +export class BodyLoggingInterceptor implements NestInterceptor { + constructor(@InjectPinoLogger(BodyLoggingInterceptor.name) private readonly logger: PinoLogger) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const { method, url, body } = request; + if (body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && Object.keys(body).length > 0) { + this.logger.debug({ method, url, body: mask(body) }, 'request body'); + } + return next.handle(); + } +} diff --git a/src/logging/logger.config.ts b/src/logging/logger.config.ts index 7895fc9..d10deee 100644 --- a/src/logging/logger.config.ts +++ b/src/logging/logger.config.ts @@ -47,6 +47,18 @@ export function createLoggerModule(): DynamicModule { pinoHttp: { level, transport: { targets }, + serializers: { + req: (req: any) => ({ + id: req.id, + method: req.method, + url: req.url, + query: req.query, + params: req.params, + remoteAddress: req.remoteAddress, + remotePort: req.remotePort, + }), + res: (res: any) => ({ statusCode: res.statusCode }), + }, mixin() { const src = getCaller(); return src ? { src } : {};