@nest-omni/core 4.1.3-25 → 4.1.3-27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cache/cache.service.d.ts +3 -1
- package/cache/cache.service.js +8 -8
- package/cache/decorators/cache-put.decorator.js +5 -4
- package/cache/dependencies/callback.dependency.js +9 -0
- package/cache/dependencies/tag.dependency.d.ts +1 -9
- package/cache/dependencies/tag.dependency.js +5 -14
- package/cache/providers/lrucache.provider.d.ts +1 -0
- package/cache/providers/lrucache.provider.js +6 -4
- package/cache/providers/redis-cache.provider.d.ts +1 -0
- package/cache/providers/redis-cache.provider.js +8 -6
- package/http-client/config/http-client.config.js +4 -2
- package/http-client/decorators/http-client.decorators.d.ts +1 -1
- package/http-client/decorators/http-client.decorators.js +1 -1
- package/http-client/examples/advanced-usage.example.js +3 -3
- package/http-client/examples/axios-config-extended.example.js +1 -3
- package/http-client/examples/flexible-response-example.d.ts +28 -0
- package/http-client/examples/flexible-response-example.js +120 -0
- package/http-client/examples/ssl-certificate.example.d.ts +2 -2
- package/http-client/examples/ssl-certificate.example.js +18 -17
- package/http-client/http-client.module.js +2 -2
- package/http-client/interfaces/http-client-config.interface.d.ts +6 -1
- package/http-client/services/api-client-registry.service.d.ts +6 -6
- package/http-client/services/api-client-registry.service.js +9 -9
- package/http-client/services/circuit-breaker.service.d.ts +9 -9
- package/http-client/services/circuit-breaker.service.js +24 -24
- package/http-client/services/http-client.service.d.ts +30 -13
- package/http-client/services/http-client.service.js +76 -47
- package/http-client/services/logging.service.d.ts +17 -33
- package/http-client/services/logging.service.js +81 -169
- package/http-client/utils/curl-generator.util.js +2 -5
- package/http-client/utils/index.d.ts +1 -0
- package/http-client/utils/index.js +1 -0
- package/http-client/utils/proxy-environment.util.d.ts +12 -12
- package/http-client/utils/proxy-environment.util.js +25 -19
- package/http-client/utils/retry-recorder.util.d.ts +0 -4
- package/http-client/utils/retry-recorder.util.js +2 -27
- package/http-client/utils/sanitize.util.d.ts +58 -0
- package/http-client/utils/sanitize.util.js +188 -0
- package/http-client/utils/security-validator.util.d.ts +19 -19
- package/http-client/utils/security-validator.util.js +66 -64
- package/interceptors/http-logging-interceptor.service.d.ts +38 -0
- package/interceptors/http-logging-interceptor.service.js +167 -0
- package/interceptors/index.d.ts +1 -0
- package/interceptors/index.js +1 -0
- package/package.json +1 -1
- package/setup/bootstrap.setup.js +1 -1
- package/shared/services/api-config.service.js +3 -18
- package/vault/vault-config.service.js +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SanitizeUtil = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* HTTP 请求数据脱敏工具
|
|
6
|
+
* 统一处理 headers、body、query string 的敏感信息过滤
|
|
7
|
+
*/
|
|
8
|
+
class SanitizeUtil {
|
|
9
|
+
/**
|
|
10
|
+
* 脱敏 headers
|
|
11
|
+
* @param headers - 原始 headers 对象
|
|
12
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
13
|
+
* @returns 脱敏后的 headers
|
|
14
|
+
*/
|
|
15
|
+
static sanitizeHeaders(headers, sensitiveFields = []) {
|
|
16
|
+
if (!headers)
|
|
17
|
+
return {};
|
|
18
|
+
const sanitized = {};
|
|
19
|
+
const fieldsToSanitize = [
|
|
20
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
21
|
+
...sensitiveFields,
|
|
22
|
+
];
|
|
23
|
+
Object.keys(headers).forEach((key) => {
|
|
24
|
+
if (this.isSensitiveField(key, fieldsToSanitize)) {
|
|
25
|
+
sanitized[key] = '[FILTERED]';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
sanitized[key] = String(headers[key]);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return sanitized;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 脱敏 body
|
|
35
|
+
* @param body - 原始 body 数据
|
|
36
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
37
|
+
* @returns 脱敏后的 body
|
|
38
|
+
*/
|
|
39
|
+
static sanitizeBody(body, sensitiveFields = []) {
|
|
40
|
+
if (!body)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (typeof body === 'string') {
|
|
43
|
+
try {
|
|
44
|
+
body = JSON.parse(body);
|
|
45
|
+
}
|
|
46
|
+
catch (_a) {
|
|
47
|
+
// 非 JSON 字符串,不做处理
|
|
48
|
+
return body.length > 1000 ? body.substring(0, 1000) + '...' : body;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (typeof body === 'object') {
|
|
52
|
+
const sanitized = Object.assign({}, body);
|
|
53
|
+
const fieldsToSanitize = [
|
|
54
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
55
|
+
...sensitiveFields,
|
|
56
|
+
];
|
|
57
|
+
this.sanitizeObject(sanitized, fieldsToSanitize);
|
|
58
|
+
return sanitized;
|
|
59
|
+
}
|
|
60
|
+
return body;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 脱敏 URL 中的 query string
|
|
64
|
+
* @param url - 原始 URL
|
|
65
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
66
|
+
* @returns 脱敏后的 URL
|
|
67
|
+
*/
|
|
68
|
+
static sanitizeQueryString(url, sensitiveFields = []) {
|
|
69
|
+
if (!url)
|
|
70
|
+
return url;
|
|
71
|
+
const queryIndex = url.indexOf('?');
|
|
72
|
+
if (queryIndex === -1) {
|
|
73
|
+
return url; // 没有 query string
|
|
74
|
+
}
|
|
75
|
+
const baseUrl = url.substring(0, queryIndex);
|
|
76
|
+
const queryString = url.substring(queryIndex + 1);
|
|
77
|
+
const params = new URLSearchParams(queryString);
|
|
78
|
+
const fieldsToSanitize = [
|
|
79
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
80
|
+
...sensitiveFields,
|
|
81
|
+
];
|
|
82
|
+
params.forEach((value, key) => {
|
|
83
|
+
if (this.isSensitiveField(key, fieldsToSanitize)) {
|
|
84
|
+
params.set(key, '[FILTERED]');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const sanitizedQuery = params.toString();
|
|
88
|
+
return sanitizedQuery ? `${baseUrl}?${sanitizedQuery}` : baseUrl;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 脱敏 params 对象
|
|
92
|
+
* @param params - 原始 params 对象
|
|
93
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
94
|
+
* @returns 脱敏后的 params
|
|
95
|
+
*/
|
|
96
|
+
static sanitizeParams(params, sensitiveFields = []) {
|
|
97
|
+
if (!params)
|
|
98
|
+
return {};
|
|
99
|
+
const sanitized = {};
|
|
100
|
+
const fieldsToSanitize = [
|
|
101
|
+
...this.DEFAULT_SENSITIVE_FIELDS,
|
|
102
|
+
...sensitiveFields,
|
|
103
|
+
];
|
|
104
|
+
Object.keys(params).forEach((key) => {
|
|
105
|
+
if (this.isSensitiveField(key, fieldsToSanitize)) {
|
|
106
|
+
sanitized[key] = '[FILTERED]';
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
sanitized[key] = params[key];
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return sanitized;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 将 body 转换为字符串并脱敏
|
|
116
|
+
* @param data - 原始数据
|
|
117
|
+
* @param sensitiveFields - 自定义敏感字段列表
|
|
118
|
+
* @returns 脱敏后的字符串
|
|
119
|
+
*/
|
|
120
|
+
static sanitizeBodyAsString(data, sensitiveFields = []) {
|
|
121
|
+
const sanitized = this.sanitizeBody(data, sensitiveFields);
|
|
122
|
+
if (!sanitized)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (typeof sanitized === 'string') {
|
|
125
|
+
return sanitized.length > 5000
|
|
126
|
+
? sanitized.substring(0, 5000) + '...'
|
|
127
|
+
: sanitized;
|
|
128
|
+
}
|
|
129
|
+
const jsonString = JSON.stringify(sanitized);
|
|
130
|
+
return jsonString.length > 5000
|
|
131
|
+
? jsonString.substring(0, 5000) + '...'
|
|
132
|
+
: jsonString;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 判断字段是否为敏感字段
|
|
136
|
+
* @param key - 字段名
|
|
137
|
+
* @param sensitiveFields - 敏感字段列表
|
|
138
|
+
* @returns 是否为敏感字段
|
|
139
|
+
*/
|
|
140
|
+
static isSensitiveField(key, sensitiveFields) {
|
|
141
|
+
const lowerKey = key.toLowerCase();
|
|
142
|
+
return sensitiveFields.some((field) => lowerKey.includes(field.toLowerCase()));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 递归脱敏对象中的敏感字段
|
|
146
|
+
* @param obj - 目标对象
|
|
147
|
+
* @param sensitiveFields - 敏感字段列表
|
|
148
|
+
*/
|
|
149
|
+
static sanitizeObject(obj, sensitiveFields) {
|
|
150
|
+
if (typeof obj !== 'object' || obj === null)
|
|
151
|
+
return;
|
|
152
|
+
for (const key in obj) {
|
|
153
|
+
if (this.isSensitiveField(key, sensitiveFields)) {
|
|
154
|
+
obj[key] = '[FILTERED]';
|
|
155
|
+
}
|
|
156
|
+
else if (typeof obj[key] === 'object') {
|
|
157
|
+
this.sanitizeObject(obj[key], sensitiveFields);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.SanitizeUtil = SanitizeUtil;
|
|
163
|
+
/**
|
|
164
|
+
* 默认敏感字段列表
|
|
165
|
+
*/
|
|
166
|
+
SanitizeUtil.DEFAULT_SENSITIVE_FIELDS = [
|
|
167
|
+
// Headers 相关
|
|
168
|
+
'authorization',
|
|
169
|
+
'apikey',
|
|
170
|
+
'x-api-key',
|
|
171
|
+
'x-auth-token',
|
|
172
|
+
'cookie',
|
|
173
|
+
'set-cookie',
|
|
174
|
+
// Body/Query 通用字段
|
|
175
|
+
'password',
|
|
176
|
+
'secret',
|
|
177
|
+
'token',
|
|
178
|
+
'key',
|
|
179
|
+
'credential',
|
|
180
|
+
'private',
|
|
181
|
+
'confidential',
|
|
182
|
+
'ssn',
|
|
183
|
+
'creditCard',
|
|
184
|
+
'accessToken',
|
|
185
|
+
'refreshToken',
|
|
186
|
+
'apiKey',
|
|
187
|
+
'apiSecret',
|
|
188
|
+
];
|
|
@@ -75,6 +75,25 @@ export declare class SecurityValidator {
|
|
|
75
75
|
valid: boolean;
|
|
76
76
|
error?: string;
|
|
77
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* 敏感数据检测
|
|
80
|
+
* 检查数据中是否包含敏感信息(如密码、token等)
|
|
81
|
+
* @param data 要检查的数据对象
|
|
82
|
+
* @param additionalPatterns 额外的敏感数据模式
|
|
83
|
+
* @returns 检测结果
|
|
84
|
+
*/
|
|
85
|
+
static detectSensitiveData(data: any, additionalPatterns?: RegExp[]): {
|
|
86
|
+
hasSensitiveData: boolean;
|
|
87
|
+
fields: string[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* 获取默认SSRF防护配置
|
|
91
|
+
*/
|
|
92
|
+
static getDefaultSSRFConfig(): SSRFProtectionConfig;
|
|
93
|
+
/**
|
|
94
|
+
* 获取默认URL验证配置
|
|
95
|
+
*/
|
|
96
|
+
static getDefaultURLConfig(): URLValidationConfig;
|
|
78
97
|
/**
|
|
79
98
|
* 从主机名中提取IP地址
|
|
80
99
|
*/
|
|
@@ -96,23 +115,4 @@ export declare class SecurityValidator {
|
|
|
96
115
|
* 验证主机名格式
|
|
97
116
|
*/
|
|
98
117
|
private static isValidHostname;
|
|
99
|
-
/**
|
|
100
|
-
* 敏感数据检测
|
|
101
|
-
* 检查数据中是否包含敏感信息(如密码、token等)
|
|
102
|
-
* @param data 要检查的数据对象
|
|
103
|
-
* @param additionalPatterns 额外的敏感数据模式
|
|
104
|
-
* @returns 检测结果
|
|
105
|
-
*/
|
|
106
|
-
static detectSensitiveData(data: any, additionalPatterns?: RegExp[]): {
|
|
107
|
-
hasSensitiveData: boolean;
|
|
108
|
-
fields: string[];
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
* 获取默认SSRF防护配置
|
|
112
|
-
*/
|
|
113
|
-
static getDefaultSSRFConfig(): SSRFProtectionConfig;
|
|
114
|
-
/**
|
|
115
|
-
* 获取默认URL验证配置
|
|
116
|
-
*/
|
|
117
|
-
static getDefaultURLConfig(): URLValidationConfig;
|
|
118
118
|
}
|
|
@@ -143,6 +143,69 @@ class SecurityValidator {
|
|
|
143
143
|
}
|
|
144
144
|
return { url: sanitizedURL, valid: true };
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* 敏感数据检测
|
|
148
|
+
* 检查数据中是否包含敏感信息(如密码、token等)
|
|
149
|
+
* @param data 要检查的数据对象
|
|
150
|
+
* @param additionalPatterns 额外的敏感数据模式
|
|
151
|
+
* @returns 检测结果
|
|
152
|
+
*/
|
|
153
|
+
static detectSensitiveData(data, additionalPatterns = []) {
|
|
154
|
+
const sensitiveFields = [];
|
|
155
|
+
// 默认敏感字段名模式
|
|
156
|
+
const defaultPatterns = [
|
|
157
|
+
/password/i,
|
|
158
|
+
/secret/i,
|
|
159
|
+
/token/i,
|
|
160
|
+
/api[_-]?key/i,
|
|
161
|
+
/authorization/i,
|
|
162
|
+
/credential/i,
|
|
163
|
+
/private[_-]?key/i,
|
|
164
|
+
/access[_-]?token/i,
|
|
165
|
+
/refresh[_-]?token/i,
|
|
166
|
+
/session[_-]?id/i,
|
|
167
|
+
/csrf/i,
|
|
168
|
+
/ssn/i,
|
|
169
|
+
/credit[_-]?card/i,
|
|
170
|
+
];
|
|
171
|
+
const allPatterns = [...defaultPatterns, ...additionalPatterns];
|
|
172
|
+
const checkObject = (obj, path = '') => {
|
|
173
|
+
if (!obj || typeof obj !== 'object') {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
for (const key in obj) {
|
|
177
|
+
if (!obj.hasOwnProperty(key)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
181
|
+
// 检查键名是否匹配敏感模式
|
|
182
|
+
if (allPatterns.some((pattern) => pattern.test(key))) {
|
|
183
|
+
sensitiveFields.push(currentPath);
|
|
184
|
+
}
|
|
185
|
+
// 递归检查嵌套对象
|
|
186
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
187
|
+
checkObject(obj[key], currentPath);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
checkObject(data);
|
|
192
|
+
return {
|
|
193
|
+
hasSensitiveData: sensitiveFields.length > 0,
|
|
194
|
+
fields: sensitiveFields,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 获取默认SSRF防护配置
|
|
199
|
+
*/
|
|
200
|
+
static getDefaultSSRFConfig() {
|
|
201
|
+
return Object.assign({}, this.defaultSSRFConfig);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 获取默认URL验证配置
|
|
205
|
+
*/
|
|
206
|
+
static getDefaultURLConfig() {
|
|
207
|
+
return Object.assign({}, this.defaultURLConfig);
|
|
208
|
+
}
|
|
146
209
|
/**
|
|
147
210
|
* 从主机名中提取IP地址
|
|
148
211
|
*/
|
|
@@ -165,7 +228,9 @@ class SecurityValidator {
|
|
|
165
228
|
static validateIPAddress(ipAddress, config) {
|
|
166
229
|
// 检查是否为本地回环地址
|
|
167
230
|
if (!config.allowLoopback) {
|
|
168
|
-
if (ipAddress === '127.0.0.1' ||
|
|
231
|
+
if (ipAddress === '127.0.0.1' ||
|
|
232
|
+
ipAddress === '::1' ||
|
|
233
|
+
ipAddress.startsWith('127.')) {
|
|
169
234
|
return {
|
|
170
235
|
valid: false,
|
|
171
236
|
error: 'Loopback addresses are not allowed',
|
|
@@ -251,69 +316,6 @@ class SecurityValidator {
|
|
|
251
316
|
const hostnameRegex = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$/;
|
|
252
317
|
return hostnameRegex.test(hostname);
|
|
253
318
|
}
|
|
254
|
-
/**
|
|
255
|
-
* 敏感数据检测
|
|
256
|
-
* 检查数据中是否包含敏感信息(如密码、token等)
|
|
257
|
-
* @param data 要检查的数据对象
|
|
258
|
-
* @param additionalPatterns 额外的敏感数据模式
|
|
259
|
-
* @returns 检测结果
|
|
260
|
-
*/
|
|
261
|
-
static detectSensitiveData(data, additionalPatterns = []) {
|
|
262
|
-
const sensitiveFields = [];
|
|
263
|
-
// 默认敏感字段名模式
|
|
264
|
-
const defaultPatterns = [
|
|
265
|
-
/password/i,
|
|
266
|
-
/secret/i,
|
|
267
|
-
/token/i,
|
|
268
|
-
/api[_-]?key/i,
|
|
269
|
-
/authorization/i,
|
|
270
|
-
/credential/i,
|
|
271
|
-
/private[_-]?key/i,
|
|
272
|
-
/access[_-]?token/i,
|
|
273
|
-
/refresh[_-]?token/i,
|
|
274
|
-
/session[_-]?id/i,
|
|
275
|
-
/csrf/i,
|
|
276
|
-
/ssn/i,
|
|
277
|
-
/credit[_-]?card/i,
|
|
278
|
-
];
|
|
279
|
-
const allPatterns = [...defaultPatterns, ...additionalPatterns];
|
|
280
|
-
const checkObject = (obj, path = '') => {
|
|
281
|
-
if (!obj || typeof obj !== 'object') {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
for (const key in obj) {
|
|
285
|
-
if (!obj.hasOwnProperty(key)) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
289
|
-
// 检查键名是否匹配敏感模式
|
|
290
|
-
if (allPatterns.some((pattern) => pattern.test(key))) {
|
|
291
|
-
sensitiveFields.push(currentPath);
|
|
292
|
-
}
|
|
293
|
-
// 递归检查嵌套对象
|
|
294
|
-
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
295
|
-
checkObject(obj[key], currentPath);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
checkObject(data);
|
|
300
|
-
return {
|
|
301
|
-
hasSensitiveData: sensitiveFields.length > 0,
|
|
302
|
-
fields: sensitiveFields,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* 获取默认SSRF防护配置
|
|
307
|
-
*/
|
|
308
|
-
static getDefaultSSRFConfig() {
|
|
309
|
-
return Object.assign({}, this.defaultSSRFConfig);
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* 获取默认URL验证配置
|
|
313
|
-
*/
|
|
314
|
-
static getDefaultURLConfig() {
|
|
315
|
-
return Object.assign({}, this.defaultURLConfig);
|
|
316
|
-
}
|
|
317
319
|
}
|
|
318
320
|
exports.SecurityValidator = SecurityValidator;
|
|
319
321
|
SecurityValidator.logger = new common_1.Logger(SecurityValidator.name);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { ApiConfigService } from '../shared/services/api-config.service';
|
|
4
|
+
/**
|
|
5
|
+
* HTTP 日志拦截器
|
|
6
|
+
* 参考 Tomcat AccessLog 的实现,每个请求只记录一条日志
|
|
7
|
+
* 在请求完成时同时记录请求和响应的完整信息
|
|
8
|
+
*/
|
|
9
|
+
export declare class HttpLoggingInterceptor implements NestInterceptor {
|
|
10
|
+
private readonly configService;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
constructor(configService: ApiConfigService);
|
|
13
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
|
|
14
|
+
/**
|
|
15
|
+
* 生成请求 ID
|
|
16
|
+
*/
|
|
17
|
+
private generateRequestId;
|
|
18
|
+
/**
|
|
19
|
+
* 记录请求和响应(一条日志)
|
|
20
|
+
*/
|
|
21
|
+
private logRequestResponse;
|
|
22
|
+
/**
|
|
23
|
+
* 记录请求和错误(一条日志)
|
|
24
|
+
*/
|
|
25
|
+
private logRequestError;
|
|
26
|
+
/**
|
|
27
|
+
* 清理敏感的 header 信息
|
|
28
|
+
*/
|
|
29
|
+
private sanitizeHeaders;
|
|
30
|
+
/**
|
|
31
|
+
* 清理敏感的 body 信息
|
|
32
|
+
*/
|
|
33
|
+
private sanitizeBody;
|
|
34
|
+
/**
|
|
35
|
+
* 从 headers 提取用户信息
|
|
36
|
+
*/
|
|
37
|
+
private extractUser;
|
|
38
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var HttpLoggingInterceptor_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.HttpLoggingInterceptor = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const operators_1 = require("rxjs/operators");
|
|
16
|
+
const api_config_service_1 = require("../shared/services/api-config.service");
|
|
17
|
+
/**
|
|
18
|
+
* HTTP 日志拦截器
|
|
19
|
+
* 参考 Tomcat AccessLog 的实现,每个请求只记录一条日志
|
|
20
|
+
* 在请求完成时同时记录请求和响应的完整信息
|
|
21
|
+
*/
|
|
22
|
+
let HttpLoggingInterceptor = HttpLoggingInterceptor_1 = class HttpLoggingInterceptor {
|
|
23
|
+
constructor(configService) {
|
|
24
|
+
this.configService = configService;
|
|
25
|
+
this.logger = new common_1.Logger(HttpLoggingInterceptor_1.name);
|
|
26
|
+
}
|
|
27
|
+
intercept(context, next) {
|
|
28
|
+
const ctx = context.switchToHttp();
|
|
29
|
+
const request = ctx.getRequest();
|
|
30
|
+
const response = ctx.getResponse();
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
const { method, url, headers, body } = request;
|
|
33
|
+
const requestId = String(request.id || headers['x-request-id'] || this.generateRequestId());
|
|
34
|
+
// 监听响应完成事件,一条日志同时记录请求和响应
|
|
35
|
+
response.on('finish', () => {
|
|
36
|
+
const duration = Date.now() - startTime;
|
|
37
|
+
this.logRequestResponse(requestId, request, response, duration);
|
|
38
|
+
});
|
|
39
|
+
return next.handle().pipe((0, operators_1.catchError)((error) => {
|
|
40
|
+
const duration = Date.now() - startTime;
|
|
41
|
+
this.logRequestError(requestId, request, error, duration);
|
|
42
|
+
throw error;
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 生成请求 ID
|
|
47
|
+
*/
|
|
48
|
+
generateRequestId() {
|
|
49
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 记录请求和响应(一条日志)
|
|
53
|
+
*/
|
|
54
|
+
logRequestResponse(requestId, request, response, duration) {
|
|
55
|
+
const statusCode = response.statusCode;
|
|
56
|
+
const logData = {
|
|
57
|
+
requestId,
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
env: this.configService.nodeEnv,
|
|
60
|
+
appName: this.configService.getString('NAME'),
|
|
61
|
+
http: {
|
|
62
|
+
method: request.method,
|
|
63
|
+
url: request.url,
|
|
64
|
+
query: request.query,
|
|
65
|
+
statusCode,
|
|
66
|
+
duration: `${duration}ms`,
|
|
67
|
+
req: {
|
|
68
|
+
headers: this.sanitizeHeaders(request.headers),
|
|
69
|
+
body: this.sanitizeBody(request.body),
|
|
70
|
+
},
|
|
71
|
+
res: {
|
|
72
|
+
headers: this.sanitizeHeaders(response.getHeaders()),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
user: this.extractUser(request.headers),
|
|
76
|
+
};
|
|
77
|
+
const message = `HTTP ${request.method} ${request.url} ${statusCode} (${duration}ms)`;
|
|
78
|
+
if (statusCode >= 500) {
|
|
79
|
+
this.logger.error(message, logData);
|
|
80
|
+
}
|
|
81
|
+
else if (statusCode >= 400) {
|
|
82
|
+
this.logger.warn(message, logData);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.logger.log(message, logData);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 记录请求和错误(一条日志)
|
|
90
|
+
*/
|
|
91
|
+
logRequestError(requestId, request, error, duration) {
|
|
92
|
+
const logData = {
|
|
93
|
+
requestId,
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
env: this.configService.nodeEnv,
|
|
96
|
+
appName: this.configService.getString('NAME'),
|
|
97
|
+
http: {
|
|
98
|
+
method: request.method,
|
|
99
|
+
url: request.url,
|
|
100
|
+
query: request.query,
|
|
101
|
+
statusCode: (error === null || error === void 0 ? void 0 : error.status) || 500,
|
|
102
|
+
duration: `${duration}ms`,
|
|
103
|
+
req: {
|
|
104
|
+
headers: this.sanitizeHeaders(request.headers),
|
|
105
|
+
body: this.sanitizeBody(request.body),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
error: {
|
|
109
|
+
message: error.message,
|
|
110
|
+
stack: error.stack,
|
|
111
|
+
code: error.code,
|
|
112
|
+
},
|
|
113
|
+
user: this.extractUser(request.headers),
|
|
114
|
+
};
|
|
115
|
+
this.logger.error(`HTTP ${request.method} ${request.url} ${(error === null || error === void 0 ? void 0 : error.status) || 500} (${duration}ms) - ${error.message}`, logData);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 清理敏感的 header 信息
|
|
119
|
+
*/
|
|
120
|
+
sanitizeHeaders(headers) {
|
|
121
|
+
if (!headers)
|
|
122
|
+
return {};
|
|
123
|
+
const sanitized = Object.assign({}, headers);
|
|
124
|
+
const sensitiveKeys = ['authorization', 'apikey', 'x-api-key', 'token', 'cookie'];
|
|
125
|
+
for (const key of Object.keys(sanitized)) {
|
|
126
|
+
if (sensitiveKeys.includes(key.toLowerCase())) {
|
|
127
|
+
sanitized[key] = '[REDACTED]';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return sanitized;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 清理敏感的 body 信息
|
|
134
|
+
*/
|
|
135
|
+
sanitizeBody(body) {
|
|
136
|
+
if (!body)
|
|
137
|
+
return body;
|
|
138
|
+
// 如果 body 太大,只记录部分信息
|
|
139
|
+
const bodyStr = JSON.stringify(body);
|
|
140
|
+
if (bodyStr.length > 1000) {
|
|
141
|
+
return { _large: `${bodyStr.length} bytes` };
|
|
142
|
+
}
|
|
143
|
+
return body;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 从 headers 提取用户信息
|
|
147
|
+
*/
|
|
148
|
+
extractUser(headers) {
|
|
149
|
+
const userHeader = headers['user'] || headers['x-user'];
|
|
150
|
+
if (userHeader) {
|
|
151
|
+
try {
|
|
152
|
+
return typeof userHeader === 'string'
|
|
153
|
+
? JSON.parse(userHeader)
|
|
154
|
+
: userHeader;
|
|
155
|
+
}
|
|
156
|
+
catch (_a) {
|
|
157
|
+
return userHeader;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
exports.HttpLoggingInterceptor = HttpLoggingInterceptor;
|
|
164
|
+
exports.HttpLoggingInterceptor = HttpLoggingInterceptor = HttpLoggingInterceptor_1 = __decorate([
|
|
165
|
+
(0, common_1.Injectable)(),
|
|
166
|
+
__metadata("design:paramtypes", [api_config_service_1.ApiConfigService])
|
|
167
|
+
], HttpLoggingInterceptor);
|
package/interceptors/index.d.ts
CHANGED
package/interceptors/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./language-interceptor.service"), exports);
|
|
18
18
|
__exportStar(require("./translation-interceptor.service"), exports);
|
|
19
|
+
__exportStar(require("./http-logging-interceptor.service"), exports);
|
package/package.json
CHANGED
package/setup/bootstrap.setup.js
CHANGED
|
@@ -166,7 +166,7 @@ function bootstrapSetup(AppModule, SetupSwagger) {
|
|
|
166
166
|
const reflector = app.get(core_1.Reflector);
|
|
167
167
|
app.useGlobalFilters(new setup_1.SentryGlobalFilter(), new __1.HttpExceptionFilter(), new __1.QueryFailedFilter(reflector));
|
|
168
168
|
// 全局拦截器
|
|
169
|
-
app.useGlobalInterceptors(new __1.
|
|
169
|
+
app.useGlobalInterceptors(new __1.HttpLoggingInterceptor(configService), new __1.LanguageInterceptor(), new __1.TranslationInterceptor());
|
|
170
170
|
// 全局管道
|
|
171
171
|
app.useGlobalPipes(new nestjs_i18n_1.I18nValidationPipe({
|
|
172
172
|
whitelist: true,
|
|
@@ -214,25 +214,10 @@ let ApiConfigService = ApiConfigService_1 = class ApiConfigService {
|
|
|
214
214
|
genReqId: (req) => req.id,
|
|
215
215
|
transport,
|
|
216
216
|
level: this.isDev ? 'debug' : 'info',
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return 'error';
|
|
220
|
-
if (res.statusCode >= 400)
|
|
221
|
-
return 'warn';
|
|
222
|
-
return 'info';
|
|
223
|
-
},
|
|
224
|
-
wrapSerializers: true,
|
|
225
|
-
customProps: (req, res) => {
|
|
226
|
-
return {
|
|
227
|
-
env: this.nodeEnv,
|
|
228
|
-
appName: this.getString('NAME'),
|
|
229
|
-
user: req === null || req === void 0 ? void 0 : req.user,
|
|
230
|
-
'req.body': req === null || req === void 0 ? void 0 : req.body,
|
|
231
|
-
'res.body': res === null || res === void 0 ? void 0 : res.body,
|
|
232
|
-
};
|
|
233
|
-
},
|
|
217
|
+
// 禁用 pinoHttp 的自动日志,由 HttpLoggingInterceptor 处理
|
|
218
|
+
autoLogging: false,
|
|
234
219
|
redact: {
|
|
235
|
-
paths: ['req.headers.authorization', 'req.headers.apikey'],
|
|
220
|
+
paths: ['req.headers.authorization', 'req.headers.apikey', 'req.headers.cookie'],
|
|
236
221
|
remove: true,
|
|
237
222
|
},
|
|
238
223
|
},
|
|
@@ -172,7 +172,7 @@ let VaultConfigService = VaultConfigService_1 = class VaultConfigService {
|
|
|
172
172
|
}
|
|
173
173
|
try {
|
|
174
174
|
const health = yield this.vaultClient.health();
|
|
175
|
-
return health && !health.sealed;
|
|
175
|
+
return !!(health && !health.sealed);
|
|
176
176
|
}
|
|
177
177
|
catch (error) {
|
|
178
178
|
this.logger.error('Vault health check failed', error.message);
|