chanjs 2.7.4 → 2.7.6
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/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/BaseComponent.js +27 -0
- package/core/Container.js +68 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +323 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +101 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +251 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +61 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +15 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +176 -197
- package/package.json +9 -2
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +84 -139
- package/security/keywords.js +33 -137
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +58 -198
- package/storage/index.js +3 -6
- package/storage/redis.js +124 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +20 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +95 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- package/schedule/schedule.js +0 -491
package/security/sign.js
CHANGED
|
@@ -1,210 +1,117 @@
|
|
|
1
|
-
import crypto from "crypto";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
* 提供安全的 AES-256-GCM 加密解密和 HMAC-SHA256 数据签名
|
|
6
|
-
*
|
|
7
|
-
* 安全改进:
|
|
8
|
-
* 1. 弃用 CryptoJS(默认用 EVP_BytesToKey 派生密钥,安全性不足)
|
|
9
|
-
* 2. 改用 Node.js 原生 crypto 模块,AES-256-GCM 提供机密性 + 完整性
|
|
10
|
-
* 3. 密钥派生用 scrypt(抗暴力破解,NIST 推荐)
|
|
11
|
-
* 4. verifySign 用 crypto.timingSafeEqual 替换 ===,防止时序攻击
|
|
12
|
-
*
|
|
13
|
-
* 兼容性说明:
|
|
14
|
-
* - 新版 AES 密文格式:base64(iv:tag:ciphertext),与 CryptoJS 不兼容
|
|
15
|
-
* - 旧版 Cookie 失效后业务层 catch 会自动重新生成(Share.js 已处理)
|
|
2
|
+
* AES-256-GCM 加解密工具,内置scrypt密钥派生+LRU缓存
|
|
16
3
|
*/
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import logger from "../utils/logger.js";
|
|
6
|
+
|
|
7
|
+
const AES_CFG = Object.freeze({
|
|
8
|
+
ALG: "aes-256-gcm",
|
|
9
|
+
IV_LEN: 12,
|
|
10
|
+
TAG_LEN: 16,
|
|
11
|
+
KEY_BYTES: 32,
|
|
12
|
+
SCRYPT_N: 16384,
|
|
13
|
+
SCRYPT_R: 8,
|
|
14
|
+
SCRYPT_P: 1,
|
|
15
|
+
CACHE_MAX: 100,
|
|
16
|
+
});
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* 密钥派生缓存(避免每次加解密都派生一遍)
|
|
20
|
-
* 改为 LRU:命中时先 delete 再 set,刷新访问顺序
|
|
21
|
-
*/
|
|
22
18
|
const keyCache = new Map();
|
|
23
|
-
|
|
19
|
+
let saltWarnFlag = false;
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* 安全改进:
|
|
30
|
-
* - salt 优先用 AES_SALT 环境变量(部署期固定),不再从 secret 推导
|
|
31
|
-
* - 未设置 AES_SALT 时降级到原行为并打印警告
|
|
32
|
-
* @private
|
|
22
|
+
* scrypt派生32字节AES密钥,带LRU缓存复用
|
|
23
|
+
* @param {string} secret
|
|
24
|
+
* @returns {Promise<Buffer|null>}
|
|
33
25
|
*/
|
|
34
|
-
|
|
35
|
-
if (!secret || typeof secret !==
|
|
26
|
+
const deriveKey = async secret => {
|
|
27
|
+
if (!secret || typeof secret !== "string") return null;
|
|
28
|
+
|
|
29
|
+
// LRU 命中
|
|
36
30
|
if (keyCache.has(secret)) {
|
|
37
|
-
|
|
38
|
-
const cached = keyCache.get(secret);
|
|
31
|
+
const buf = keyCache.get(secret);
|
|
39
32
|
keyCache.delete(secret);
|
|
40
|
-
keyCache.set(secret,
|
|
41
|
-
return
|
|
33
|
+
keyCache.set(secret, buf);
|
|
34
|
+
return buf;
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
//
|
|
45
|
-
const aesSalt = process.env.AES_SALT;
|
|
37
|
+
// 派生盐:环境变量优先,否则警告并用 secret 派生
|
|
46
38
|
let salt;
|
|
47
|
-
if (
|
|
48
|
-
salt = crypto.createHash(
|
|
39
|
+
if (process.env.AES_SALT) {
|
|
40
|
+
salt = crypto.createHash("sha256").update(process.env.AES_SALT).digest();
|
|
49
41
|
} else {
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
if (!saltWarnFlag) {
|
|
43
|
+
logger.warn("[安全警告] 未配置环境变量 AES_SALT,建议统一配置固定盐值");
|
|
44
|
+
saltWarnFlag = true;
|
|
53
45
|
}
|
|
54
|
-
salt = crypto.createHash(
|
|
46
|
+
salt = crypto.createHash("sha256").update(secret).digest();
|
|
55
47
|
}
|
|
56
|
-
const key = crypto.scryptSync(secret, salt, 32); // AES-256 需要 32 字节密钥
|
|
57
48
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
// scrypt 异步派生
|
|
50
|
+
const keyBuf = await new Promise((resolve, reject) => {
|
|
51
|
+
crypto.scrypt(secret, salt, AES_CFG.KEY_BYTES, {
|
|
52
|
+
N: AES_CFG.SCRYPT_N, r: AES_CFG.SCRYPT_R, p: AES_CFG.SCRYPT_P,
|
|
53
|
+
}, (err, derivedKey) => err ? reject(err) : resolve(derivedKey));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// LRU 淘汰
|
|
57
|
+
if (keyCache.size >= AES_CFG.CACHE_MAX) {
|
|
58
|
+
const oldest = keyCache.keys().next().value;
|
|
59
|
+
if (oldest !== undefined) keyCache.delete(oldest);
|
|
62
60
|
}
|
|
63
|
-
keyCache.set(secret,
|
|
64
|
-
return
|
|
65
|
-
}
|
|
61
|
+
keyCache.set(secret, keyBuf);
|
|
62
|
+
return keyBuf;
|
|
63
|
+
};
|
|
66
64
|
|
|
67
65
|
/**
|
|
68
|
-
* AES
|
|
69
|
-
* @param {
|
|
70
|
-
* @param {string}
|
|
71
|
-
* @returns {string|null}
|
|
72
|
-
* @description
|
|
73
|
-
* 使用 AES-256-GCM 算法加密数据
|
|
74
|
-
* - GCM 模式提供机密性 + 完整性校验
|
|
75
|
-
* - 每次加密生成随机 IV(12 字节),防止相同明文产生相同密文
|
|
76
|
-
* - 输出格式:base64(iv + tag + ciphertext)
|
|
77
|
-
* @example
|
|
78
|
-
* const encrypted = aesEncrypt({ id: 1, name: '张三' }, 'my-secret-key');
|
|
66
|
+
* AES加密:输出iv+tag+密文合并base64
|
|
67
|
+
* @param {string|object} data
|
|
68
|
+
* @param {string} secret
|
|
69
|
+
* @returns {Promise<string|null>}
|
|
79
70
|
*/
|
|
80
|
-
export const aesEncrypt = (
|
|
81
|
-
if (!
|
|
82
|
-
console.error('[安全错误] AES_SALT 必须配置');
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
71
|
+
export const aesEncrypt = async (data, secret) => {
|
|
72
|
+
if (!secret) return logger.error("[安全错误] 加密密钥secret不能为空"), null;
|
|
85
73
|
try {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const tag = cipher.getAuthTag(); // 16 字节认证标签
|
|
99
|
-
|
|
100
|
-
// 格式:base64(iv + tag + ciphertext)
|
|
101
|
-
return Buffer.concat([iv, tag, encrypted]).toString('base64');
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error("AES加密失败:", error.message);
|
|
74
|
+
const key = await deriveKey(secret);
|
|
75
|
+
if (!key) return null;
|
|
76
|
+
|
|
77
|
+
const iv = crypto.randomBytes(AES_CFG.IV_LEN);
|
|
78
|
+
const cipher = crypto.createCipheriv(AES_CFG.ALG, key, iv);
|
|
79
|
+
const plain = typeof data === "string" ? data : JSON.stringify(data);
|
|
80
|
+
|
|
81
|
+
const cipherBuf = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
82
|
+
const tag = cipher.getAuthTag();
|
|
83
|
+
return Buffer.concat([iv, tag, cipherBuf]).toString("base64");
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error("AES加密失败", err.message);
|
|
104
86
|
return null;
|
|
105
87
|
}
|
|
106
88
|
};
|
|
107
89
|
|
|
108
90
|
/**
|
|
109
|
-
* AES
|
|
110
|
-
* @param {string}
|
|
111
|
-
* @param {string}
|
|
112
|
-
* @returns {string|null}
|
|
113
|
-
* @description
|
|
114
|
-
* 使用 AES-256-GCM 算法解密数据
|
|
115
|
-
* - 自动校验 GCM 认证标签,密文被篡改会抛错
|
|
116
|
-
* - 与 aesEncrypt 配对使用
|
|
117
|
-
* @example
|
|
118
|
-
* const decrypted = aesDecrypt(encryptedStr, 'my-secret-key');
|
|
91
|
+
* AES解密,校验GCM完整性tag防篡改
|
|
92
|
+
* @param {string} base64Str
|
|
93
|
+
* @param {string} secret
|
|
94
|
+
* @returns {Promise<string|null>}
|
|
119
95
|
*/
|
|
120
|
-
export const aesDecrypt = (
|
|
121
|
-
if (!
|
|
122
|
-
console.error('[安全错误] AES_SALT 必须配置');
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
96
|
+
export const aesDecrypt = async (base64Str, secret) => {
|
|
97
|
+
if (!secret) return logger.error("[安全错误] 解密密钥secret不能为空"), null;
|
|
125
98
|
try {
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
99
|
+
const key = await deriveKey(secret);
|
|
100
|
+
if (!key) return null;
|
|
128
101
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
const iv = data.subarray(0, 12);
|
|
133
|
-
const tag = data.subarray(12, 28);
|
|
134
|
-
const ciphertext = data.subarray(28);
|
|
102
|
+
const raw = Buffer.from(base64Str, "base64");
|
|
103
|
+
const minLen = AES_CFG.IV_LEN + AES_CFG.TAG_LEN;
|
|
104
|
+
if (raw.length < minLen) return null;
|
|
135
105
|
|
|
136
|
-
const
|
|
137
|
-
|
|
106
|
+
const iv = raw.subarray(0, AES_CFG.IV_LEN);
|
|
107
|
+
const tag = raw.subarray(AES_CFG.IV_LEN, minLen);
|
|
108
|
+
const cipherBuf = raw.subarray(minLen);
|
|
138
109
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.error("AES解密失败:", error.message);
|
|
110
|
+
const decipher = crypto.createDecipheriv(AES_CFG.ALG, key, iv);
|
|
111
|
+
decipher.setAuthTag(tag);
|
|
112
|
+
return Buffer.concat([decipher.update(cipherBuf), decipher.final()]).toString("utf8");
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger.error("AES解密失败", err.message);
|
|
146
115
|
return null;
|
|
147
116
|
}
|
|
148
117
|
};
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 数据签名(HMAC-SHA256)
|
|
152
|
-
* @param {Object} data - 要签名的数据对象
|
|
153
|
-
* @param {string} secret - 签名密钥
|
|
154
|
-
* @returns {string|null} 签名字符串(hex 编码),失败时返回 null
|
|
155
|
-
* @description
|
|
156
|
-
* 使用 HMAC-SHA256 算法对数据进行签名
|
|
157
|
-
* 原生 crypto 实现,与 CryptoJS.HmacSHA256 输出格式兼容(hex 字符串)
|
|
158
|
-
* @example
|
|
159
|
-
* const signature = signData({ userId: 123 }, 'my-sign-key');
|
|
160
|
-
*/
|
|
161
|
-
export function signData(data, secret) {
|
|
162
|
-
if (!secret) {
|
|
163
|
-
console.error('[安全错误] 签名密钥必须配置');
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
const hmac = crypto.createHmac('sha256', secret);
|
|
168
|
-
hmac.update(JSON.stringify(data));
|
|
169
|
-
return hmac.digest('hex');
|
|
170
|
-
} catch (error) {
|
|
171
|
-
console.error("数据签名失败:", error.message);
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 验证数据签名
|
|
178
|
-
* @param {Object} data - 原始数据对象
|
|
179
|
-
* @param {string} signature - 待验证的签名
|
|
180
|
-
* @param {string} secret - 签名密钥
|
|
181
|
-
* @returns {boolean} 签名是否有效
|
|
182
|
-
* @description
|
|
183
|
-
* 重新计算签名并使用 crypto.timingSafeEqual 进行常量时间比对
|
|
184
|
-
* 防止时序攻击:攻击者无法通过响应时间推断签名前缀
|
|
185
|
-
*
|
|
186
|
-
* 安全要点:
|
|
187
|
-
* 1. 两边长度不同直接返回 false(不进入 timingSafeEqual,避免越界)
|
|
188
|
-
* 2. 长度相同才用 timingSafeEqual 进行常量时间比较
|
|
189
|
-
* 3. 返回值不抛异常,只返回 true/false
|
|
190
|
-
* @example
|
|
191
|
-
* const isValid = verifySign({ userId: 123 }, signature, 'my-sign-key');
|
|
192
|
-
*/
|
|
193
|
-
export function verifySign(data, signature, secret) {
|
|
194
|
-
try {
|
|
195
|
-
const computedSign = signData(data, secret);
|
|
196
|
-
if (!computedSign || !signature) return false;
|
|
197
|
-
|
|
198
|
-
// 长度不同直接返回 false(timingSafeEqual 要求两边等长)
|
|
199
|
-
if (computedSign.length !== signature.length) return false;
|
|
200
|
-
|
|
201
|
-
// 常量时间比较,防止时序攻击
|
|
202
|
-
return crypto.timingSafeEqual(
|
|
203
|
-
Buffer.from(computedSign),
|
|
204
|
-
Buffer.from(String(signature))
|
|
205
|
-
);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
console.error("签名验证失败:", error.message);
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
package/security/xss-filter.js
CHANGED
|
@@ -1,63 +1,31 @@
|
|
|
1
|
-
import xss from
|
|
1
|
+
import xss from "xss";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* XSS
|
|
5
|
-
*
|
|
4
|
+
* 递归深度过滤对象/数组/字符串XSS,WeakSet防循环引用栈溢出
|
|
5
|
+
* @param {any} data 待清洗数据
|
|
6
|
+
* @param {WeakSet} [visited] 循环引用标记缓存
|
|
7
|
+
* @returns {any} 清洗后数据
|
|
6
8
|
*/
|
|
9
|
+
export const filterXSS = (data, visited) => {
|
|
10
|
+
// 字符串直接过滤
|
|
11
|
+
if (typeof data === "string") return xss(data);
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* @param {*} data - 要过滤的数据,可以是字符串、数组或对象
|
|
11
|
-
* @returns {*} 过滤后的数据
|
|
12
|
-
* @description
|
|
13
|
-
* 递归过滤数据中的所有字符串值
|
|
14
|
-
* 使用 xss 库清除危险的 HTML 和 JavaScript 代码
|
|
15
|
-
* 支持字符串、数组和对象的递归处理
|
|
16
|
-
*
|
|
17
|
-
* 安全加固(P2 #22):
|
|
18
|
-
* 用 WeakSet 跟踪已访问对象,避免循环引用导致栈溢出
|
|
19
|
-
* - 首次遇到对象时加入 visited
|
|
20
|
-
* - 递归处理子属性时传递 visited
|
|
21
|
-
* - 遇到已访问对象直接返回(断开循环)
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* const clean = filterXSS({
|
|
25
|
-
* name: '<script>alert(1)</script>',
|
|
26
|
-
* items: ['<img src=x onerror=alert(1)>']
|
|
27
|
-
* });
|
|
28
|
-
*/
|
|
29
|
-
export function filterXSS(data, visited) {
|
|
30
|
-
// 字符串:xss 过滤
|
|
31
|
-
if (typeof data === 'string') {
|
|
32
|
-
return xss(data);
|
|
33
|
-
}
|
|
13
|
+
// 数组遍历递归
|
|
14
|
+
if (Array.isArray(data)) return data.map(item => filterXSS(item, visited));
|
|
34
15
|
|
|
35
|
-
//
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 对象:递归处理每个属性
|
|
43
|
-
if (data && typeof data === 'object') {
|
|
44
|
-
// 循环引用检测
|
|
45
|
-
if (!visited) visited = new WeakSet();
|
|
46
|
-
if (visited.has(data)) {
|
|
47
|
-
// 已访问过,返回空对象断开循环(保留类型一致性)
|
|
48
|
-
return {};
|
|
49
|
-
}
|
|
50
|
-
visited.add(data);
|
|
16
|
+
// 普通对象处理,循环引用截断
|
|
17
|
+
if (data && typeof data === "object") {
|
|
18
|
+
const vis = visited ?? new WeakSet();
|
|
19
|
+
if (vis.has(data)) return {};
|
|
20
|
+
vis.add(data);
|
|
51
21
|
|
|
52
|
-
const
|
|
53
|
-
for (const key
|
|
54
|
-
|
|
55
|
-
result[key] = filterXSS(data[key], visited);
|
|
56
|
-
}
|
|
22
|
+
const target = {};
|
|
23
|
+
for (const key of Object.keys(data)) {
|
|
24
|
+
target[key] = filterXSS(data[key], vis);
|
|
57
25
|
}
|
|
58
|
-
return
|
|
26
|
+
return target;
|
|
59
27
|
}
|
|
60
28
|
|
|
61
|
-
//
|
|
29
|
+
// number / boolean / null / undefined 直接原值返回
|
|
62
30
|
return data;
|
|
63
|
-
}
|
|
31
|
+
};
|