6-6 作业解答:全局FIilters&如何获取请求IP
全局异常过滤器的唯一性
核心特性详解
1. 单例约束机制
- 设计原理:NestJS采用装饰器模式实现过滤器链,全局过滤器通过
APP_FILTER
令牌注册为单例 - 验证方法:尝试注册多个全局过滤器时,控制台会输出警告信息:
[Nest] WARN Multiple global filters detected, only the first one will be used.
bash - 框架源码定位:可查看
@nestjs/core/exceptions/exception-handler.ts
中的handleError
方法
2. 异常捕获范围
异常类型 | 示例 | 触发场景 |
---|---|---|
HTTP异常 | NotFoundException | 访问不存在的路由 |
业务逻辑异常 | CustomBusinessException | 违反业务规则时抛出 |
系统级异常 | TypeError | 未定义变量操作 |
WebSocket异常 | WsException | WebSocket连接中断 |
第三方库异常 | MongooseError | MongoDB查询失败 |
3. 执行优先级
关键实现进阶
完整异常处理器示例
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: 500;
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: this.getErrorMessage(exception)
});
}
private getErrorMessage(exception: unknown): string {
if (exception instanceof Error) {
return exception.message;
}
return 'Unexpected error occurred';
}
}
typescript
注册方式对比
- 模块注册(推荐):
@Module({ providers: [ { provide: APP_FILTER, useClass: GlobalExceptionFilter } ] }) export class AppModule {}
typescript - main.ts注册:
async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new GlobalExceptionFilter()); await app.listen(3000); }
typescript
常见问题解答
Q1:为什么我的自定义异常没有被捕获?
A:检查是否继承了HttpException
,非HTTP异常需使用@Catch()
无参数形式捕获
Q2:如何给特定路由禁用全局过滤器?
A:使用@UseFilters()
覆盖全局过滤器:
@Get('special-route')
@UseFilters(SpecialFilter) // 优先级高于全局过滤器
getSpecial() {}
typescript
Q3:生产环境需要额外配置吗?
A:建议添加以下增强措施:
- 集成Sentry/Rollbar等错误监控系统
- 实现告警机制(如邮件通知)
- 设置错误率阈值监控
最佳实践建议
- 日志记录:在过滤器中记录完整的错误堆栈
console.error('Exception stack:', exception.stack);
typescript - 错误分类:对不同类型异常采用不同处理策略
if (exception instanceof DatabaseError) { // 数据库错误特殊处理 }
typescript - 性能优化:避免在过滤器中执行耗时操作
💡扩展思考:可以尝试实现一个支持错误码映射的增强版过滤器,将异常类型转换为预定义的业务错误码体系。
客户端IP获取方案深度解析
核心方案实现
1. request-ip库的完整应用
import { Request } from 'express';
import { getClientIp } from 'request-ip';
// 增强版IP获取中间件
export function clientIpMiddleware(req: Request) {
const ip = getClientIp(req);
// 验证IP格式
if (!isValidIp(ip)) {
throw new Error(`Invalid IP format: ${ip}`);
}
// 附加到请求对象供后续使用
req.clientIp = ip;
return ip;
}
function isValidIp(ip: string): boolean {
return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(ip) ||
/^([a-f0-9:]+:+)+[a-f0-9]+$/.test(ip); // IPv4/IPv6校验
}
typescript
2. 生产环境推荐配置
// 安全配置示例
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
// 代理层级设置(根据实际架构调整)
app.set('trust proxy', 3); // 信任3层代理
typescript
技术原理深入
代理头信息解析机制
头字段 | 使用场景 | 格式示例 |
---|---|---|
X-Forwarded-For | 通用代理链 | client, proxy1, proxy2 |
CF-Connecting-IP | Cloudflare专用 | 203.0.113.1 |
True-Client-Ip | Akamai/AWS ELB | 198.51.100.2 |
X-Real-IP | Nginx默认 | 192.0.2.1 |
原生方案增强实现
function getClientIpManual(req: Request): string {
const headers = [
'x-client-ip',
'x-forwarded-for',
'cf-connecting-ip',
'true-client-ip',
'x-real-ip'
];
for (const header of headers) {
const value = req.headers[header];
if (value) {
if (Array.isArray(value)) {
return value[0].split(',')[0].trim();
}
return value.split(',')[0].trim();
}
}
return req.socket.remoteAddress || '0.0.0.0';
}
typescript
企业级解决方案
1. IP地理信息集成
import { geoip } from 'geoip-lite';
function getIpInfo(ip: string) {
const geo = geoip.lookup(ip);
return {
ip,
country: geo?.country,
city: geo?.city,
timezone: geo?.timezone
};
}
typescript
2. 限流防护实现
import { RateLimiterMemory } from 'rate-limiter-flexible';
const limiter = new RateLimiterMemory({
points: 100, // 100次请求
duration: 60 // 60秒内
});
async function rateLimitMiddleware(req: Request) {
try {
await limiter.consume(req.clientIp);
return true;
} catch (rej) {
throw new HttpException('Too Many Requests', 429);
}
}
typescript
常见问题解决方案
Q1:为什么获取到的是127.0.0.1?
- 检查Nginx配置是否缺少
proxy_set_header X-Real-IP $remote_addr;
- 确认Express的
trust proxy
设置正确
Q2:如何应对IP伪造攻击?
// IP白名单验证
const WHITE_LIST = ['192.168.1.0/24'];
function validateIp(ip: string) {
return WHITE_LIST.some(range => {
return ip.startsWith(range.split('/')[0]);
});
}
typescript
Q3:IPv6地址如何处理?
// 统一转换工具
function normalizeIp(ip: string) {
return ip.replace(/^::ffff:/, ''); // 转换IPv4映射的IPv6地址
}
typescript
性能优化建议
- 缓存机制:对频繁访问的IP进行缓存
const ipCache = new Map<string, IpInfo>(); function getCachedIp(ip: string) { if (ipCache.has(ip)) { return ipCache.get(ip); } const info = getIpInfo(ip); ipCache.set(ip, info); return info; }
typescript - 异步处理:将IP查询转为非阻塞操作
async function asyncGetIp(ip: string) { return new Promise((resolve) => { setImmediate(() => resolve(getIpInfo(ip))); }); }
typescript
💡 专家提示:在Kubernetes环境中,需要特别注意Service Mesh(如Istio)的代理层可能增加额外的X-Forwarded-For跳数,建议通过app.set('trust proxy', true)
自动处理。
请求元数据记录规范深度解析
日志数据结构增强版
1. 完整日志模型设计
interface AuditLog {
// 基础信息
traceId: string; // 分布式追踪ID
spanId: string; // 调用链SpanID
timestamp: string; // ISO8601扩展格式
duration: number; // 请求处理时长(ms)
// 网络层信息
ip: string;
protocol: string; // HTTP/1.1或HTTP/2
method: string;
path: string;
statusCode: number;
// 参数信息
headers: Record<string, string>;
query: Record<string, any>;
params: Record<string, any>;
body: any;
// 上下文信息
userAgent: string;
referrer: string;
hostname: string;
// 异常信息
error?: {
message: string;
stack?: string;
code?: string; // 自定义错误码
};
// 性能指标
memoryUsage?: NodeJS.MemoryUsage;
}
typescript
2. 敏感信息过滤策略
function sanitizeHeaders(headers: Record<string, string>) {
const sensitiveFields = ['authorization', 'cookie', 'x-api-key'];
return Object.fromEntries(
Object.entries(headers).map(([k, v]) =>
[k, sensitiveFields.includes(k.toLowerCase()) ? '***REDACTED***' : v]
)
);
}
typescript
日志存储进阶方案
1. 多目标日志输出配置
// winston-config.ts
import { createLogger, transports, format } from 'winston';
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
format.json()
),
transports: [
// 控制台输出(开发环境)
new transports.Console({
format: format.combine(
format.colorize(),
format.simple()
)
}),
// 文件输出(生产环境)
new transports.DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d'
}),
// ELK集成(生产环境)
new transports.Http({
host: 'logstash.example.com',
port: 5044,
ssl: true
})
]
});
typescript
2. 日志分级策略
级别 | 使用场景 | 示例内容 |
---|---|---|
error | 系统错误 | 未捕获异常/数据库连接失败 |
warn | 预期外但可恢复的问题 | API限流触发/参数校验失败 |
info | 业务关键操作 | 用户登录/订单创建 |
debug | 开发调试信息 | SQL查询语句/中间件执行细节 |
verbose | 详细跟踪信息 | 请求头完整内容 |
安全增强措施
1. GDPR合规处理
function gdprCompliantLog(data: any) {
return JSON.stringify(data, (key, value) => {
if (['email', 'phone', 'credit_card'].includes(key)) {
return value.toString().replace(/.(?=.{4})/g, '*');
}
return value;
});
}
typescript
2. 日志加密存储
import { createCipheriv } from 'crypto';
function encryptLog(log: string) {
const algorithm = 'aes-256-cbc';
const key = process.env.LOG_ENCRYPTION_KEY;
const iv = crypto.randomBytes(16);
const cipher = createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(log, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
}
typescript
性能优化技巧
1. 异步日志写入
import { Worker } from 'worker_threads';
function asyncLog(logData: AuditLog) {
new Worker('./log-worker.js', {
workerData: logData
});
}
// log-worker.js
parentPort.on('message', (data) => {
logger.info(data);
});
typescript
2. 采样率控制
const sampleRate = 0.1; // 10%的请求记录详细日志
function shouldLogDetailed(): boolean {
return Math.random() < sampleRate;
}
typescript
企业级实践案例
1. 全链路追踪集成
import { context, trace } from '@opentelemetry/api';
function enrichWithTrace(log: AuditLog) {
const activeSpan = trace.getActiveSpan();
return {
...log,
traceId: activeSpan?.spanContext().traceId,
spanId: activeSpan?.spanContext().spanId,
traceFlags: activeSpan?.spanContext().traceFlags
};
}
typescript
2. 审计日志合规存储
# 日志存储架构
logs/
├── audit/ # 审计日志(保留7年)
│ ├── 2023/
│ │ ├── Q1/
│ │ └── Q2/
├── application/ # 应用日志(保留1年)
└── performance/ # 性能日志(保留3个月)
bash
常见问题解决方案
Q1:如何平衡日志详细度和性能?
- 采用动态日志级别:
logger.level = process.env.NODE_ENV === 'production' ? 'info' : 'debug'
- 使用条件日志记录:
if (logger.isDebugEnabled()) logger.debug(heavyData)
Q2:日志文件过大如何处理?
- 配置日志轮转:
new transports.DailyRotateFile({ filename: 'logs/app-%DATE%.log', maxFiles: '30d', // 保留30天 maxSize: '100m' // 单文件最大100MB })
typescript
Q3:如何快速检索特定请求日志?
- 注入唯一请求ID:
app.use((req, res, next) => { req.requestId = crypto.randomUUID(); next(); });
typescript
可视化监控方案
💡 专家建议:对于金融级应用,建议实现日志的区块链存证,使用Hyperledger Fabric等框架将关键审计日志写入不可篡改的分布式账本。
异常响应标准化深度指南
增强版响应规范
1. 完整错误响应模型
interface ErrorResponse {
// 基础信息
requestId: string; // 唯一请求标识
timestamp: string; // ISO8601扩展格式
path: string; // 请求路径
method: string; // HTTP方法
// 错误信息
code: string; // 业务错误码
message: string; // 用户友好消息
documentationUrl?: string; // 错误文档链接
// 调试信息
details?: { // 验证错误详情
field: string;
message: string;
}[];
stack?: string; // 开发环境堆栈
}
typescript
2. 多语言支持实现
function getLocalizedMessage(errorCode: string, lang = 'en') {
const messages = {
'404': {
en: 'Resource not found',
zh: '资源不存在'
},
'500': {
en: 'Internal server error',
zh: '服务器内部错误'
}
};
return messages[errorCode]?.[lang] || messages[errorCode]?.en;
}
typescript
生产环境安全策略
1. 敏感数据过滤
function sanitizeError(error: Error) {
return {
...error,
message: error.message.replace(/(password|token)=[^&]+/g, '***REDACTED***'),
stack: process.env.NODE_ENV === 'production'
? undefined
: error.stack
};
}
typescript
2. 错误分类处理
switch (true) {
case exception instanceof DatabaseError:
response.status(503).json({
code: 'DB_001',
message: 'Service unavailable'
});
break;
case exception instanceof AuthError:
response.status(401).json({
code: 'AUTH_002',
message: 'Invalid credentials'
});
break;
default:
response.status(500).json({
code: 'UNKNOWN',
message: 'Internal error'
});
}
typescript
开发调试增强
1. 错误追踪集成
if (!isProduction) {
response.data.trace = {
correlationId: crypto.randomUUID(),
sessionId: req.session?.id,
debugHint: 'Add ?debug=true to get more info'
};
}
typescript
2. 交互式调试支持
if (req.query.debug === 'true') {
response.data.interactiveDebug = {
query: req.query,
environment: process.env.NODE_ENV,
memoryUsage: process.memoryUsage()
};
}
typescript
企业级最佳实践
1. 错误码标准化体系
错误类型 | 前缀 | 示例 | HTTP状态码 |
---|---|---|---|
业务错误 | BIZ_ | BIZ_ORDER_PAID | 400 |
认证错误 | AUTH_ | AUTH_EXPIRED | 401 |
系统错误 | SYS_ | SYS_DB_FAIL | 500 |
第三方服务错误 | EXT_ | EXT_PAY_FAIL | 502 |
2. 错误文档自动生成
function generateErrorDocs() {
return Object.values(errorCatalog).map(err => ({
code: err.code,
description: err.description,
solutions: err.solutions,
curlExample: `curl -X GET ${apiBaseUrl}/errors/${err.code}`
}));
}
typescript
性能优化方案
1. 错误响应缓存
const errorCache = new Map();
function getCachedErrorResponse(code: string) {
if (errorCache.has(code)) {
return errorCache.get(code);
}
const response = buildErrorResponse(code);
errorCache.set(code, response);
return response;
}
typescript
2. 轻量级错误格式
// 适用于高频API
function minimalError(code: string) {
return {
e: code, // 错误码缩写
t: Date.now() // 时间戳
};
}
typescript
监控与告警集成
常见问题解决方案
Q1:如何防止敏感信息泄露?
- 使用正则过滤敏感字段:
function redactSensitive(text: string) { return text .replace(/(password|token|ssn)=[^&]+/g, '$1=***') .replace(/"\w*password"\s*:\s*".+?"/g, '"password":"***"'); }
typescript
Q2:如何支持前端错误分类处理?
- 返回标准错误结构:
{ "error": { "category": "validation|auth|business", "fields": ["username"], "retryable": false } }
typescript
Q3:如何记录错误上下文?
- 使用CLS(Continuation Local Storage):
const cls = require('cls-hooked'); namespace = cls.createNamespace('app'); // 中间件中设置上下文 namespace.run(() => { namespace.set('requestId', uuid()); next(); });
typescript
扩展思考
- 错误恢复建议:在响应中包含可操作的修复建议
{ "suggestions": [ "Try refreshing your auth token", "Check API documentation at /docs" ] }
typescript - 机器学习集成:使用历史错误数据训练模型预测潜在故障
- A/B测试支持:为不同用户群体返回不同错误详情级别
💡 专家提示:对于金融/医疗等敏感行业,建议实现错误响应的双重加密:先用应用级密钥加密,再用每个会话的临时密钥二次加密。
验证与测试指南深度扩展
完整的测试矩阵
1. 异常场景测试用例
测试类型 | 触发方式 | 预期结果 | 验证要点 |
---|---|---|---|
无效路由 | GET /non-existent-route | 404状态码+标准错误格式 | 错误消息本地化支持 |
参数校验失败 | POST /users {age: "abc"} | 400状态码+详细字段错误 | 错误详情数组结构 |
认证失效 | 不带Token访问保护路由 | 401状态码+WWW-Authenticate头 | 包含realm信息 |
权限不足 | 普通用户访问管理员接口 | 403状态码+明确拒绝原因 | 错误码分类准确 |
服务不可用 | 关闭数据库连接 | 503状态码+retry-after头 | 服务降级响应 |
2. 自动化测试脚本
// test/error-handling.e2e-spec.ts
describe('Exception Filter', () => {
it('should return 404 for invalid route', async () => {
const response = await request(app)
.get('/non-existent-route')
.expect(404);
expect(response.body).toMatchObject({
statusCode: 404,
path: '/non-existent-route',
message: expect.stringContaining('Not Found')
});
});
it('should sanitize database errors', async () => {
jest.spyOn(userService, 'findOne').mockRejectedValue(
new DatabaseError('Connection failed')
);
const response = await request(app)
.get('/users/123')
.expect(500);
expect(response.body.message).not.toContain('Connection failed');
});
});
typescript
高级验证技术
1. 混沌工程测试
# 使用chaosblade模拟网络延迟
blade create network delay --time 3000 --interface eth0 --remote-port 3000
bash
2. 负载测试验证
# 使用k6进行错误率测试
k6 run --vus 100 --duration 30s script.js
bash
// script.js
import http from 'k6/http';
import { check } from 'k6';
export default function() {
const res = http.get('http://api.example.com/non-existent-route');
check(res, {
'is 404': (r) => r.status === 404,
'has correct body': (r) => JSON.parse(r.body).statusCode === 404
});
}
javascript
日志分析进阶
1. 结构化日志查询
# 使用jq分析错误日志
cat error.log | jq 'select(.statusCode == 500) | {path, timestamp}'
bash
2. 实时监控告警
# 使用awk实现错误率报警
tail -f error.log | awk '/statusCode":5[0-9]{2}/ {count++} END {if(count>10) system("send-alert.sh")}'
bash
工具链增强
1. API测试工具扩展
工具 | 特色功能 | 适用场景 |
---|---|---|
Postman | 可视化场景测试+Mock服务 | 接口调试/文档生成 |
Insomnia | 代码生成+环境变量管理 | 团队协作开发 |
HTTPie | CLI友好+语法高亮 | 快速调试/脚本集成 |
Paw | 高级断言+自动化导出 | Mac平台深度用户 |
2. 日志分析工具推荐
1. **ELK Stack**
- 实时日志分析+可视化仪表盘
- 示例查询:`status:500 AND path:/api/v1*`
2. **Grafana Loki**
- 轻量级日志聚合系统
- 优势:低资源消耗+Prometheus集成
3. **Splunk**
- 企业级日志分析
- 特色:机器学习异常检测
markdown
生产环境验证清单
- 压力测试指标
- 错误率 < 0.1% (HTTP 5xx)
- 99%请求响应时间 < 500ms
- 日志丢失率 = 0%
- 监控看板配置
- 灾备演练方案
- 每月模拟日志服务中断
- 季度性全链路故障演练
常见问题排查指南
Q1:测试环境与生产环境行为不一致?
- 检查差异点:
diff <(curl test-env/api) <(curl prod-env/api)
bash - 常见原因:环境变量/中间件版本/数据量差异
Q2:如何验证错误日志完整性?
- 使用日志指纹校验:
# 验证日志连续性 from hashlib import md5 prev_hash = '' for line in open('error.log'): current_hash = md5(line.encode()).hexdigest() assert prev_hash in line or not prev_hash, "Log break detected" prev_hash = current_hash
python
Q3:自动化测试如何覆盖所有错误场景?
- 采用变异测试:
// 使用Stryker进行变异测试 module.exports = { mutator: 'javascript', packageManager: 'npm', reporters: ['html', 'clear-text'], testRunner: 'jest', coverageAnalysis: 'all' };
javascript
扩展实践
- 错误注入测试
使用服务网格工具(如Istio)注入故障:# Istio VirtualService配置 apiVersion: networking.istio.io/v1alpha3 kind: VirtualService spec: http: - fault: delay: percentage: 10 fixedDelay: 5s route: - destination: host: my-service
yaml - AI辅助分析
训练模型预测错误根源:# 使用日志训练分类模型 from sklearn.ensemble import RandomForestClassifier clf = RandomForestClassifier() clf.fit(log_features, error_causes)
python - 全链路追踪验证
通过TraceID关联日志:# 使用Jaeger查询 jaeger-cli --server-url=http://jaeger:16686 trace 1a2b3c4d
bash
💡 专家提示:建立错误模式知识库,将历史故障处理方案结构化存储,推荐使用GitHub Issues模板或Confluence文档模板标准化错误处理流程。
↑