chanjs 2.1.1 → 2.3.1
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/App.js +384 -0
- package/base/Context.js +78 -0
- package/base/Controller.js +137 -0
- package/base/Database.js +314 -0
- package/base/Service.js +539 -0
- package/common/api.js +25 -0
- package/common/category.js +22 -0
- package/common/code.js +42 -0
- package/common/email.js +110 -0
- package/common/index.js +7 -0
- package/common/pages.js +86 -0
- package/common/sms.js +104 -0
- package/common/utils.js +73 -0
- package/config/code.js +110 -52
- package/config/index.js +10 -0
- package/config/paths.js +60 -0
- package/extend/art-template.js +46 -28
- package/extend/index.js +6 -0
- package/global/env.js +11 -5
- package/global/global.js +63 -39
- package/global/import.js +43 -39
- package/global/index.js +8 -3
- package/helper/cache.js +182 -0
- package/helper/data-parse.js +121 -37
- package/helper/db.js +71 -83
- package/helper/file.js +158 -208
- package/helper/filter.js +34 -0
- package/helper/html.js +30 -47
- package/helper/index.js +29 -5
- package/helper/ip.js +48 -31
- package/helper/jwt.js +78 -11
- package/helper/loader.js +93 -50
- package/helper/request.js +41 -144
- package/helper/sign.js +96 -33
- package/helper/time.js +89 -74
- package/helper/tree.js +77 -0
- package/index.js +15 -181
- package/middleware/cookie.js +20 -4
- package/middleware/cors.js +20 -0
- package/middleware/favicon.js +21 -5
- package/middleware/header.js +26 -9
- package/middleware/index.js +14 -23
- package/middleware/preventRetry.js +30 -0
- package/middleware/setBody.js +24 -10
- package/middleware/static.js +31 -10
- package/middleware/template.js +34 -14
- package/middleware/validator.js +43 -23
- package/middleware/waf.js +147 -287
- package/package.json +1 -1
- package/utils/checker.js +68 -0
- package/utils/error-handler.js +115 -0
- package/utils/error.js +81 -0
- package/utils/index.js +6 -0
- package/utils/keywords.js +126 -0
- package/utils/rate-limit.js +116 -0
- package/utils/response.js +103 -64
- package/utils/xss-filter.js +42 -0
- package/core/controller.js +0 -33
- package/core/index.js +0 -3
- package/core/service.js +0 -307
- package/middleware/log.js +0 -21
package/helper/file.js
CHANGED
|
@@ -1,208 +1,158 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
23
|
-
* @returns {
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* @
|
|
146
|
-
*
|
|
147
|
-
*/
|
|
148
|
-
export function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// 删除文件
|
|
160
|
-
unlinkSync(link);
|
|
161
|
-
return true;
|
|
162
|
-
} catch (err) {
|
|
163
|
-
console.error(`删除图片失败: ${link}`, err);
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 递归创建目录(同步操作)
|
|
170
|
-
*
|
|
171
|
-
* @param {string} dirname - 要创建的目录路径
|
|
172
|
-
* @returns {boolean} 成功创建或目录已存在返回true,否则返回false
|
|
173
|
-
* @description 类似于mkdir -p命令,会创建所有不存在的父目录
|
|
174
|
-
*/
|
|
175
|
-
export function mkdirsSync(dirname) {
|
|
176
|
-
try {
|
|
177
|
-
// 先检查路径是否安全
|
|
178
|
-
if (!isPathSafe(dirname, APP_PATH)) {
|
|
179
|
-
console.error(`路径不安全: ${dirname}`);
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (existsSync(dirname)) {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 递归创建父目录
|
|
188
|
-
const parentDir = path.dirname(dirname);
|
|
189
|
-
if (mkdirsSync(parentDir)) {
|
|
190
|
-
mkdirSync(dirname);
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return false;
|
|
195
|
-
} catch (err) {
|
|
196
|
-
console.error(`创建目录失败: ${dirname}`, err);
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export default {
|
|
202
|
-
mkdirsSync,
|
|
203
|
-
delImg,
|
|
204
|
-
isPathSafe,
|
|
205
|
-
saveFileContent,
|
|
206
|
-
readFileContent,
|
|
207
|
-
getFileTree,
|
|
208
|
-
};
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 获取当前文件的目录路径
|
|
6
|
+
* @param {string} importMetaUrl - import.meta.url 的值
|
|
7
|
+
* @returns {string} 当前文件的目录路径
|
|
8
|
+
* @description
|
|
9
|
+
* 使用 import.meta.url 获取当前模块的 URL,然后提取目录路径
|
|
10
|
+
* 适用于 ES 模块中获取 __dirname 的替代方案
|
|
11
|
+
* @example
|
|
12
|
+
* const __dirname = dirname(import.meta.url);
|
|
13
|
+
* const configPath = path.join(__dirname, 'config.json');
|
|
14
|
+
*/
|
|
15
|
+
export function dirname(importMetaUrl) {
|
|
16
|
+
const url = new URL(importMetaUrl);
|
|
17
|
+
return path.dirname(url.pathname);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 删除图片文件
|
|
22
|
+
* @param {string} filePath - 相对于项目根目录的文件路径
|
|
23
|
+
* @returns {boolean} 如果文件存在并被删除返回 true,否则返回 false
|
|
24
|
+
* @description
|
|
25
|
+
* 删除指定路径的图片文件
|
|
26
|
+
* 路径是相对于项目根目录(process.cwd())的
|
|
27
|
+
* 如果文件不存在,不会抛出错误,直接返回 false
|
|
28
|
+
* @example
|
|
29
|
+
* const deleted = delImg('/uploads/avatar/123.jpg');
|
|
30
|
+
* if (deleted) {
|
|
31
|
+
* console.log('图片已删除');
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
export function delImg(filePath) {
|
|
35
|
+
const fullPath = path.join(process.cwd(), filePath);
|
|
36
|
+
if (fs.existsSync(fullPath)) {
|
|
37
|
+
fs.unlinkSync(fullPath);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 获取文件树结构
|
|
45
|
+
* @param {string} dirPath - 目录路径
|
|
46
|
+
* @param {boolean} [recursive=true] - 是否递归获取子目录
|
|
47
|
+
* @returns {Array} 文件树数组
|
|
48
|
+
* @description
|
|
49
|
+
* 递归读取目录下的所有文件和子目录,返回树形结构
|
|
50
|
+
* @example
|
|
51
|
+
* const tree = getFileTree('/path/to/directory');
|
|
52
|
+
* // 返回: [{ name: 'file.html', path: '/path/to/directory/file.html', type: 'file' }, ...]
|
|
53
|
+
*/
|
|
54
|
+
export function getFileTree(dirPath, recursive = true, basePath = null) {
|
|
55
|
+
if (!fs.existsSync(dirPath)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stats = fs.statSync(dirPath);
|
|
60
|
+
if (!stats.isDirectory()) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const items = [];
|
|
65
|
+
const files = fs.readdirSync(dirPath);
|
|
66
|
+
|
|
67
|
+
const base = basePath || process.cwd();
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const fullPath = path.join(dirPath, file);
|
|
71
|
+
const fileStats = fs.statSync(fullPath);
|
|
72
|
+
|
|
73
|
+
if (fileStats.isDirectory() && recursive) {
|
|
74
|
+
const children = getFileTree(fullPath, recursive, basePath);
|
|
75
|
+
items.push({
|
|
76
|
+
name: file,
|
|
77
|
+
path: fullPath,
|
|
78
|
+
type: 'directory',
|
|
79
|
+
children: children
|
|
80
|
+
});
|
|
81
|
+
} else if (fileStats.isFile()) {
|
|
82
|
+
const relativePath = path.relative(base, fullPath).replace(/\\/g, '/');
|
|
83
|
+
items.push({
|
|
84
|
+
name: file,
|
|
85
|
+
path: fullPath,
|
|
86
|
+
relativePath: relativePath.startsWith('public') ? '/' + relativePath.replace(/^\//, '') : fullPath,
|
|
87
|
+
type: 'file',
|
|
88
|
+
size: fileStats.size
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return items;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 读取文件内容
|
|
98
|
+
* @param {string} filePath - 文件路径
|
|
99
|
+
* @returns {string} 文件内容
|
|
100
|
+
* @description
|
|
101
|
+
* 读取指定文件的内容
|
|
102
|
+
* @example
|
|
103
|
+
* const content = readFileContent('/path/to/file.html');
|
|
104
|
+
*/
|
|
105
|
+
export function readFileContent(filePath) {
|
|
106
|
+
if (!fs.existsSync(filePath)) {
|
|
107
|
+
throw new Error('文件不存在');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const stats = fs.statSync(filePath);
|
|
111
|
+
if (!stats.isFile()) {
|
|
112
|
+
throw new Error('路径不是文件');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 保存文件内容
|
|
120
|
+
* @param {string} filePath - 文件路径
|
|
121
|
+
* @param {string} content - 文件内容
|
|
122
|
+
* @returns {void}
|
|
123
|
+
* @description
|
|
124
|
+
* 将内容保存到指定文件,如果目录不存在会自动创建
|
|
125
|
+
* @example
|
|
126
|
+
* saveFileContent('/path/to/file.html', '<html>...</html>');
|
|
127
|
+
*/
|
|
128
|
+
export function saveFileContent(filePath, content) {
|
|
129
|
+
const dirPath = path.dirname(filePath);
|
|
130
|
+
|
|
131
|
+
if (!fs.existsSync(dirPath)) {
|
|
132
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 检查路径是否安全
|
|
140
|
+
* @param {string} targetPath - 目标路径
|
|
141
|
+
* @param {string} basePath - 基础路径
|
|
142
|
+
* @returns {boolean} 路径是否安全
|
|
143
|
+
* @description
|
|
144
|
+
* 检查目标路径是否在基础路径范围内,防止路径遍历攻击
|
|
145
|
+
* @example
|
|
146
|
+
* const safe = isPathSafe('/project/file.html', '/project');
|
|
147
|
+
*/
|
|
148
|
+
export function isPathSafe(targetPath, basePath) {
|
|
149
|
+
const normalizedTarget = path.normalize(targetPath);
|
|
150
|
+
const normalizedBase = path.normalize(basePath);
|
|
151
|
+
|
|
152
|
+
if (normalizedTarget.includes('..')) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const relativePath = path.relative(normalizedBase, normalizedTarget);
|
|
157
|
+
return !relativePath.startsWith('..');
|
|
158
|
+
}
|
package/helper/filter.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从数组中的每个对象筛选指定字段
|
|
3
|
+
* @param {Array<Object>} data - 要处理的对象数组
|
|
4
|
+
* @param {Array<string>} fields - 需要保留的字段名数组
|
|
5
|
+
* @returns {Array<Object>} 只包含指定字段的新对象数组
|
|
6
|
+
* @description
|
|
7
|
+
* 遍历数组中的每个对象,只保留指定的字段
|
|
8
|
+
* 返回一个新的数组,不修改原始数据
|
|
9
|
+
* @example
|
|
10
|
+
* const data = [
|
|
11
|
+
* { id: 1, name: '张三', age: 25, email: 'test@example.com' },
|
|
12
|
+
* { id: 2, name: '李四', age: 30, email: 'demo@example.com' }
|
|
13
|
+
* ];
|
|
14
|
+
* const result = filterFields(data, ['id', 'name']);
|
|
15
|
+
* console.log(result);
|
|
16
|
+
* // [
|
|
17
|
+
* // { id: 1, name: '张三' },
|
|
18
|
+
* // { id: 2, name: '李四' }
|
|
19
|
+
* // ]
|
|
20
|
+
*/
|
|
21
|
+
export function filterFields(data, fields) {
|
|
22
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return data.map((item) => {
|
|
26
|
+
const filteredItem = {};
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
if (item.hasOwnProperty(field)) {
|
|
29
|
+
filteredItem[field] = item[field];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return filteredItem;
|
|
33
|
+
});
|
|
34
|
+
}
|
package/helper/html.js
CHANGED
|
@@ -1,47 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* @param {string} str -
|
|
4
|
-
* @returns {string}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// 非字符串直接返回,避免报错
|
|
32
|
-
if (typeof str !== "string") return str;
|
|
33
|
-
|
|
34
|
-
// 一次性替换所有常见实体,减少函数调用
|
|
35
|
-
return str.replace(
|
|
36
|
-
/&|<|>| |'|"/g,
|
|
37
|
-
(match) =>
|
|
38
|
-
({
|
|
39
|
-
"&": "&",
|
|
40
|
-
"<": "<",
|
|
41
|
-
">": ">",
|
|
42
|
-
" ": " ",
|
|
43
|
-
"'": "'",
|
|
44
|
-
""": '"',
|
|
45
|
-
}[match])
|
|
46
|
-
);
|
|
47
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* 解码 HTML 实体字符
|
|
3
|
+
* @param {string} str - 包含 HTML 实体的字符串
|
|
4
|
+
* @returns {string} 解码后的字符串,如果输入为空则返回空字符串
|
|
5
|
+
* @description
|
|
6
|
+
* 将 HTML 实体字符转换为对应的普通字符
|
|
7
|
+
* 支持的实体字符:
|
|
8
|
+
* - & -> &
|
|
9
|
+
* - < -> <
|
|
10
|
+
* - > -> >
|
|
11
|
+
* - " -> "
|
|
12
|
+
* - ' -> '
|
|
13
|
+
* - -> 空格
|
|
14
|
+
* @example
|
|
15
|
+
* const html = '<div>Hello & World</div>';
|
|
16
|
+
* const decoded = htmlDecode(html);
|
|
17
|
+
* console.log(decoded); // '<div>Hello & World</div>'
|
|
18
|
+
*/
|
|
19
|
+
export function htmlDecode(str) {
|
|
20
|
+
if (!str) return "";
|
|
21
|
+
const htmlEntities = {
|
|
22
|
+
"&": "&",
|
|
23
|
+
"<": "<",
|
|
24
|
+
">": ">",
|
|
25
|
+
""": '"',
|
|
26
|
+
"'": "'",
|
|
27
|
+
" ": " ",
|
|
28
|
+
};
|
|
29
|
+
return str.replace(/&[a-z]+;/gi, (match) => htmlEntities[match] || match);
|
|
30
|
+
}
|
package/helper/index.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export {
|
|
1
|
+
export { db } from "./db.js";
|
|
2
|
+
export { prefixDbConfig } from "./db.js";
|
|
3
|
+
export { loaderSort, loadConfig, clearConfigCache, bindInstance, loadController } from "./loader.js";
|
|
4
|
+
export { formatTime } from "./time.js";
|
|
5
|
+
export { formatDateFields } from "./time.js";
|
|
6
|
+
export { default as Cache } from "./cache.js";
|
|
7
|
+
export { dirname } from "./file.js";
|
|
8
|
+
export { delImg } from "./file.js";
|
|
9
|
+
export { getFileTree } from "./file.js";
|
|
10
|
+
export { readFileContent } from "./file.js";
|
|
11
|
+
export { saveFileContent } from "./file.js";
|
|
12
|
+
export { isPathSafe } from "./file.js";
|
|
13
|
+
export { htmlDecode } from "./html.js";
|
|
14
|
+
export { getIp } from "./ip.js";
|
|
15
|
+
export { verifyToken } from "./jwt.js";
|
|
16
|
+
export { generateToken } from "./jwt.js";
|
|
17
|
+
export { setToken } from "./jwt.js";
|
|
18
|
+
export { getToken } from "./jwt.js";
|
|
19
|
+
export { signData } from "./sign.js";
|
|
20
|
+
export { verifySign } from "./sign.js";
|
|
21
|
+
export { aesEncrypt } from "./sign.js";
|
|
22
|
+
export { aesDecrypt } from "./sign.js";
|
|
23
|
+
export { request } from "./request.js";
|
|
24
|
+
export { dataParse } from "./data-parse.js";
|
|
25
|
+
export { arrToObj } from "./data-parse.js";
|
|
26
|
+
export { parseJsonFields } from "./data-parse.js";
|
|
27
|
+
export { buildTree } from "./data-parse.js";
|
|
28
|
+
export { tree, treeById } from "./tree.js";
|
|
29
|
+
export { filterFields } from "./filter.js";
|
package/helper/ip.js
CHANGED
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @param {Object} req - Express请求对象
|
|
4
|
-
* @
|
|
2
|
+
* 从请求对象中获取客户端真实 IP 地址
|
|
3
|
+
* @param {Object} req - Express 请求对象
|
|
4
|
+
* @param {string} [req.ip] - Express 解析的 IP(已包含代理头部解析)
|
|
5
|
+
* @param {Object} [req.headers] - 请求头对象
|
|
6
|
+
* @param {string} [req.headers.cf-connecting-ip] - Cloudflare 连接 IP
|
|
7
|
+
* @returns {string} 客户端 IP 地址,如果无法获取则返回 "0.0.0.0"
|
|
8
|
+
* @description
|
|
9
|
+
* 从 Express 的 req.ip 获取客户端 IP。
|
|
10
|
+
*
|
|
11
|
+
* 注意:需要在 App.js 中设置 trust proxy:
|
|
12
|
+
* app.set("trust proxy", true);
|
|
13
|
+
*
|
|
14
|
+
* 设置 trust proxy 后,Express 会自动从以下头部解析真实 IP:
|
|
15
|
+
* - X-Forwarded-For(反向代理,如 Nginx)
|
|
16
|
+
* - X-Real-IP
|
|
17
|
+
* - CF-Connecting-IP(Cloudflare CDN)
|
|
18
|
+
*
|
|
19
|
+
* 此方法按优先级从以下来源获取 IP 地址:
|
|
20
|
+
* 1. req.ip(Express 自动解析,已包含代理头部处理)
|
|
21
|
+
* 2. cf-connecting-ip 请求头(Cloudflare CDN,优先级更高)
|
|
22
|
+
* 3. 默认值 "0.0.0.0"
|
|
23
|
+
*
|
|
24
|
+
* 此方法适用于部署在反向代理(如 Nginx)或 CDN 后端的应用
|
|
25
|
+
* @example
|
|
26
|
+
* app.use((req, res, next) => {
|
|
27
|
+
* const ip = getIp(req);
|
|
28
|
+
* console.log(`访问 IP: ${ip}`);
|
|
29
|
+
* next();
|
|
30
|
+
* });
|
|
5
31
|
*/
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
// 若未找到有效IP,直接返回空
|
|
21
|
-
if (!ip) return "";
|
|
22
|
-
|
|
23
|
-
// 处理多IP情况(取第一个)
|
|
24
|
-
ip = ip.split(",").shift().trim();
|
|
25
|
-
|
|
26
|
-
// IPv6转IPv4处理
|
|
27
|
-
if (ip === "::1") {
|
|
28
|
-
return "127.0.0.1"; // 本地环回地址
|
|
32
|
+
export function getIp(req) {
|
|
33
|
+
if (req.ip) {
|
|
34
|
+
const ip = req.ip;
|
|
35
|
+
|
|
36
|
+
const isLocalAddress = ip === '::1' ||
|
|
37
|
+
ip === '127.0.0.1' ||
|
|
38
|
+
ip === '0.0.0.0' ||
|
|
39
|
+
ip === 'localhost';
|
|
40
|
+
|
|
41
|
+
if (!isLocalAddress) {
|
|
42
|
+
return ip;
|
|
43
|
+
}
|
|
29
44
|
}
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
|
|
46
|
+
const headers = req.headers || {};
|
|
47
|
+
if (headers["cf-connecting-ip"]) {
|
|
48
|
+
return headers["cf-connecting-ip"];
|
|
32
49
|
}
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|