befly 2.3.3 → 3.0.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/checks/conflict.ts +329 -0
- package/checks/table.ts +252 -0
- package/config/env.ts +218 -0
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +96 -0
- package/main.ts +47 -0
- package/package.json +26 -11
- package/plugins/db.ts +60 -0
- package/plugins/logger.ts +28 -0
- package/plugins/redis.ts +47 -0
- package/scripts/syncDb/apply.ts +171 -0
- package/scripts/syncDb/constants.ts +71 -0
- package/scripts/syncDb/ddl.ts +189 -0
- package/scripts/syncDb/helpers.ts +173 -0
- package/scripts/syncDb/index.ts +203 -0
- package/scripts/syncDb/schema.ts +199 -0
- package/scripts/syncDb/sqlite.ts +50 -0
- package/scripts/syncDb/state.ts +106 -0
- package/scripts/syncDb/table.ts +214 -0
- package/scripts/syncDb/tableCreate.ts +148 -0
- package/scripts/syncDb/tests/constants.test.ts +105 -0
- package/scripts/syncDb/tests/ddl.test.ts +134 -0
- package/scripts/syncDb/tests/helpers.test.ts +70 -0
- package/scripts/syncDb/types.ts +92 -0
- package/scripts/syncDb/version.ts +73 -0
- package/scripts/syncDb.ts +10 -0
- package/tsconfig.json +58 -0
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +249 -0
- package/types/befly.d.ts +230 -0
- package/types/common.d.ts +215 -0
- package/types/context.d.ts +7 -0
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +273 -0
- package/types/index.d.ts +450 -0
- package/types/index.ts +438 -0
- package/types/jwt.d.ts +99 -0
- package/types/logger.d.ts +43 -0
- package/types/plugin.d.ts +109 -0
- package/types/redis.d.ts +46 -0
- package/types/tool.d.ts +67 -0
- package/types/validator.d.ts +43 -0
- package/types/validator.ts +43 -0
- package/utils/colors.ts +221 -0
- package/utils/crypto.ts +308 -0
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +713 -0
- package/utils/helper.ts +812 -0
- package/utils/index.ts +33 -0
- package/utils/jwt.ts +493 -0
- package/utils/logger.ts +191 -0
- package/utils/redisHelper.ts +321 -0
- package/utils/requestContext.ts +167 -0
- package/utils/sqlBuilder.ts +611 -0
- package/utils/validate.ts +493 -0
- package/utils/{xml.js → xml.ts} +100 -74
- package/.npmrc +0 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -11
- package/apis/health/info.js +0 -49
- package/apis/tool/tokenCheck.js +0 -29
- package/bin/befly.js +0 -109
- package/bunfig.toml +0 -3
- package/checks/table.js +0 -206
- package/config/env.js +0 -64
- package/main.js +0 -579
- package/plugins/db.js +0 -46
- package/plugins/logger.js +0 -14
- package/plugins/redis.js +0 -32
- package/plugins/tool.js +0 -8
- package/scripts/syncDb.js +0 -752
- package/scripts/syncDev.js +0 -96
- package/system.js +0 -118
- package/tables/common.json +0 -16
- package/tables/tool.json +0 -6
- package/utils/api.js +0 -27
- package/utils/colors.js +0 -83
- package/utils/crypto.js +0 -260
- package/utils/index.js +0 -334
- package/utils/jwt.js +0 -387
- package/utils/logger.js +0 -143
- package/utils/redisHelper.js +0 -74
- package/utils/sqlBuilder.js +0 -498
- package/utils/sqlManager.js +0 -471
- package/utils/tool.js +0 -31
- package/utils/validate.js +0 -226
package/utils/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly 工具函数集 - TypeScript 版本
|
|
3
|
+
*
|
|
4
|
+
* 本文件作为统一导出入口,从各个专用工具模块重新导出功能
|
|
5
|
+
* 保持向后兼容性,支持从 utils/index.js 导入所有工具函数
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ========== 通用辅助工具(helpers.ts)==========
|
|
9
|
+
export { Yes, No, isDebug, isType, isEmptyObject, isEmptyArray, pickFields, formatDate, calcPerfTime, cleanData, toSnakeCase, toCamelCase, keysToSnake, keysToCamel } from './helper.js';
|
|
10
|
+
export type { DataCleanOptions } from './helper.js';
|
|
11
|
+
|
|
12
|
+
// ========== 框架工具 ==========
|
|
13
|
+
export { scanAddons, getAddonDir, addonDirExists, sortPlugins, parseRule } from './helper.js';
|
|
14
|
+
|
|
15
|
+
// ========== 数据库工具 ==========
|
|
16
|
+
export { buildDatabaseUrl, createSqlClient, buildRedisUrl, createRedisClient, initDatabase, closeDatabase, initSqlOnly, initRedisOnly, getRedis, getSql, getDbHelper, isDatabaseInitialized } from './database.js';
|
|
17
|
+
|
|
18
|
+
// 导出其他大型模块
|
|
19
|
+
export { Colors } from './colors.js';
|
|
20
|
+
export { Logger } from './logger.js';
|
|
21
|
+
export { Validator } from './validate.js';
|
|
22
|
+
export { SqlBuilder } from './sqlBuilder.js';
|
|
23
|
+
export { DbHelper } from './dbHelper.js';
|
|
24
|
+
export { RedisHelper } from './redisHelper.js';
|
|
25
|
+
export { Jwt } from './jwt.js';
|
|
26
|
+
export { Crypto2 } from './crypto.js';
|
|
27
|
+
export { Xml } from './xml.js';
|
|
28
|
+
|
|
29
|
+
// 类型导出
|
|
30
|
+
export type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, TransactionCallback } from './dbHelper.js';
|
|
31
|
+
export type { JwtPayload } from './jwt.js';
|
|
32
|
+
export type { EncodingType, HashAlgorithm } from './crypto.js';
|
|
33
|
+
export type { XmlParseOptions } from './xml.js';
|
package/utils/jwt.ts
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT 工具类 - TypeScript 版本
|
|
3
|
+
* 提供JWT token的签名、验证和解码功能以及应用层的便捷接口
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHmac } from 'crypto';
|
|
7
|
+
import { Env } from '../config/env.js';
|
|
8
|
+
import type { JwtPayload, JwtSignOptions, JwtVerifyOptions, JwtAlgorithm, JwtHeader, JwtDecoded } from '../types/jwt';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* JWT 工具类
|
|
12
|
+
*/
|
|
13
|
+
export class Jwt {
|
|
14
|
+
/** 算法映射 */
|
|
15
|
+
private static readonly ALGORITHMS: Record<JwtAlgorithm, string> = {
|
|
16
|
+
HS256: 'sha256',
|
|
17
|
+
HS384: 'sha384',
|
|
18
|
+
HS512: 'sha512'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Base64 URL 编码
|
|
23
|
+
*/
|
|
24
|
+
static base64UrlEncode(input: string | Buffer): string {
|
|
25
|
+
const base64 = Buffer.isBuffer(input) ? input.toString('base64') : Buffer.from(input, 'utf8').toString('base64');
|
|
26
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Base64 URL 解码
|
|
31
|
+
*/
|
|
32
|
+
static base64UrlDecode(str: string): string {
|
|
33
|
+
let padding = 4 - (str.length % 4);
|
|
34
|
+
if (padding !== 4) str += '='.repeat(padding);
|
|
35
|
+
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
36
|
+
return Buffer.from(str, 'base64').toString('utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 解析过期时间
|
|
41
|
+
* @param expiresIn - 过期时间(数字秒数或字符串如 "1h", "7d", "-1s")
|
|
42
|
+
* @returns 秒数(可以是负数)
|
|
43
|
+
*/
|
|
44
|
+
static parseExpiration(expiresIn: string | number): number {
|
|
45
|
+
if (typeof expiresIn === 'number') return expiresIn;
|
|
46
|
+
if (typeof expiresIn !== 'string') throw new Error('过期时间格式无效');
|
|
47
|
+
|
|
48
|
+
const numericValue = parseInt(expiresIn);
|
|
49
|
+
if (!isNaN(numericValue) && numericValue.toString() === expiresIn) return numericValue;
|
|
50
|
+
|
|
51
|
+
// 支持负数时间(用于测试过期token)
|
|
52
|
+
const match = expiresIn.match(/^(-?\d+)(ms|[smhdwy])$/);
|
|
53
|
+
if (!match) throw new Error('过期时间格式无效');
|
|
54
|
+
|
|
55
|
+
const value = parseInt(match[1]); // 包含正负号
|
|
56
|
+
const unit = match[2];
|
|
57
|
+
|
|
58
|
+
if (unit === 'ms') return Math.floor(value / 1000);
|
|
59
|
+
|
|
60
|
+
const multipliers: Record<string, number> = {
|
|
61
|
+
s: 1,
|
|
62
|
+
m: 60,
|
|
63
|
+
h: 3600,
|
|
64
|
+
d: 86400,
|
|
65
|
+
w: 604800,
|
|
66
|
+
y: 31536000
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return value * multipliers[unit];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 创建签名
|
|
74
|
+
*/
|
|
75
|
+
private static createSignature(algorithm: JwtAlgorithm, secret: string, data: string): string {
|
|
76
|
+
const hashAlgorithm = this.ALGORITHMS[algorithm];
|
|
77
|
+
if (!hashAlgorithm) throw new Error(`不支持的算法: ${algorithm}`);
|
|
78
|
+
|
|
79
|
+
const hmac = createHmac(hashAlgorithm, secret);
|
|
80
|
+
hmac.update(data);
|
|
81
|
+
return this.base64UrlEncode(hmac.digest());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 常量时间比较(防止时序攻击)
|
|
86
|
+
*/
|
|
87
|
+
private static constantTimeCompare(a: string, b: string): boolean {
|
|
88
|
+
if (a.length !== b.length) return false;
|
|
89
|
+
let result = 0;
|
|
90
|
+
for (let i = 0; i < a.length; i++) {
|
|
91
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
92
|
+
}
|
|
93
|
+
return result === 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 签名 JWT token
|
|
98
|
+
* @param payload - JWT载荷数据
|
|
99
|
+
* @param options - 签名选项
|
|
100
|
+
* @returns JWT token字符串
|
|
101
|
+
*/
|
|
102
|
+
static sign(payload: JwtPayload, options: JwtSignOptions): string;
|
|
103
|
+
static sign(payload: JwtPayload, secret: string, expiresIn: string | number): string;
|
|
104
|
+
static sign(payload: JwtPayload, optionsOrSecret: JwtSignOptions | string = {}, expiresIn?: string | number): string {
|
|
105
|
+
if (!payload || typeof payload !== 'object') {
|
|
106
|
+
throw new Error('载荷必须是非空对象');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 支持两种调用方式
|
|
110
|
+
let options: JwtSignOptions;
|
|
111
|
+
if (typeof optionsOrSecret === 'string') {
|
|
112
|
+
// sign(payload, secret, expiresIn) 形式
|
|
113
|
+
options = {
|
|
114
|
+
secret: optionsOrSecret,
|
|
115
|
+
expiresIn: expiresIn
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
118
|
+
// sign(payload, options) 形式
|
|
119
|
+
options = optionsOrSecret;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const secret = options.secret || Env.JWT_SECRET;
|
|
123
|
+
const algorithm = (options.algorithm || Env.JWT_ALGORITHM || 'HS256') as JwtAlgorithm;
|
|
124
|
+
|
|
125
|
+
if (!secret) {
|
|
126
|
+
throw new Error('JWT密钥是必需的');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const now = Math.floor(Date.now() / 1000);
|
|
130
|
+
|
|
131
|
+
// 创建 header
|
|
132
|
+
const header = Jwt.base64UrlEncode(
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
alg: algorithm,
|
|
135
|
+
typ: 'JWT'
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// 创建 payload
|
|
140
|
+
const jwtPayload: JwtPayload = { ...payload, iat: now };
|
|
141
|
+
|
|
142
|
+
if (options.expiresIn || Env.JWT_EXPIRES_IN) {
|
|
143
|
+
const expSeconds = Jwt.parseExpiration(options.expiresIn || Env.JWT_EXPIRES_IN);
|
|
144
|
+
jwtPayload.exp = now + expSeconds;
|
|
145
|
+
}
|
|
146
|
+
if (options.issuer) jwtPayload.iss = options.issuer;
|
|
147
|
+
if (options.audience) jwtPayload.aud = options.audience;
|
|
148
|
+
if (options.subject) jwtPayload.sub = options.subject;
|
|
149
|
+
if (options.notBefore) {
|
|
150
|
+
jwtPayload.nbf = typeof options.notBefore === 'number' ? options.notBefore : now + Jwt.parseExpiration(options.notBefore);
|
|
151
|
+
}
|
|
152
|
+
if (options.jwtId) jwtPayload.jti = options.jwtId;
|
|
153
|
+
|
|
154
|
+
const encodedPayload = Jwt.base64UrlEncode(JSON.stringify(jwtPayload));
|
|
155
|
+
|
|
156
|
+
// 创建签名
|
|
157
|
+
const data = `${header}.${encodedPayload}`;
|
|
158
|
+
const signature = Jwt.createSignature(algorithm, secret, data);
|
|
159
|
+
|
|
160
|
+
return `${data}.${signature}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 验证 JWT token
|
|
165
|
+
* @param token - JWT token字符串
|
|
166
|
+
* @param options - 验证选项
|
|
167
|
+
* @returns 解码后的载荷数据
|
|
168
|
+
*/
|
|
169
|
+
static verify(token: string, options: JwtVerifyOptions): JwtPayload;
|
|
170
|
+
static verify(token: string, secret: string): JwtPayload;
|
|
171
|
+
static verify(token: string, optionsOrSecret: JwtVerifyOptions | string = {}): JwtPayload {
|
|
172
|
+
if (!token || typeof token !== 'string') {
|
|
173
|
+
throw new Error('Token必须是非空字符串');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 支持两种调用方式
|
|
177
|
+
let options: JwtVerifyOptions;
|
|
178
|
+
if (typeof optionsOrSecret === 'string') {
|
|
179
|
+
// verify(token, secret) 形式
|
|
180
|
+
options = { secret: optionsOrSecret };
|
|
181
|
+
} else {
|
|
182
|
+
// verify(token, options) 形式
|
|
183
|
+
options = optionsOrSecret;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const secret = options.secret || Env.JWT_SECRET;
|
|
187
|
+
if (!secret) {
|
|
188
|
+
throw new Error('JWT密钥是必需的');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const parts = token.split('.');
|
|
192
|
+
if (parts.length !== 3) {
|
|
193
|
+
throw new Error('JWT格式无效');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// 解析 header 和 payload
|
|
198
|
+
const header = JSON.parse(Jwt.base64UrlDecode(parts[0])) as JwtHeader;
|
|
199
|
+
const payload = JSON.parse(Jwt.base64UrlDecode(parts[1])) as JwtPayload;
|
|
200
|
+
const signature = parts[2];
|
|
201
|
+
|
|
202
|
+
// 验证算法
|
|
203
|
+
if (!Jwt.ALGORITHMS[header.alg]) {
|
|
204
|
+
throw new Error(`不支持的算法: ${header.alg}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 验证签名
|
|
208
|
+
const data = `${parts[0]}.${parts[1]}`;
|
|
209
|
+
const expectedSignature = Jwt.createSignature(header.alg, secret, data);
|
|
210
|
+
|
|
211
|
+
if (!Jwt.constantTimeCompare(signature, expectedSignature)) {
|
|
212
|
+
throw new Error('Token签名无效');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 验证时间
|
|
216
|
+
const now = Math.floor(Date.now() / 1000);
|
|
217
|
+
|
|
218
|
+
if (!options.ignoreExpiration && payload.exp && payload.exp < now) {
|
|
219
|
+
throw new Error('Token已过期 (expired)');
|
|
220
|
+
}
|
|
221
|
+
if (!options.ignoreNotBefore && payload.nbf && payload.nbf > now) {
|
|
222
|
+
throw new Error('Token尚未生效');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 验证 issuer、audience、subject
|
|
226
|
+
if (options.issuer && payload.iss !== options.issuer) {
|
|
227
|
+
throw new Error('Token发行者无效');
|
|
228
|
+
}
|
|
229
|
+
if (options.audience) {
|
|
230
|
+
const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
231
|
+
if (!audiences.includes(options.audience)) {
|
|
232
|
+
throw new Error('Token受众无效');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (options.subject && payload.sub !== options.subject) {
|
|
236
|
+
throw new Error('Token主题无效');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return payload;
|
|
240
|
+
} catch (error: any) {
|
|
241
|
+
if (error.message.includes('JWT') || error.message.includes('Token') || error.message.includes('无效') || error.message.includes('过期') || error.message.includes('不支持')) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
throw new Error('Token验证失败: ' + error.message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 解码 JWT token (不验证签名)
|
|
250
|
+
* @param token - JWT token字符串
|
|
251
|
+
* @param complete - 是否返回完整信息(包含header)
|
|
252
|
+
* @returns 解码后的内容
|
|
253
|
+
*/
|
|
254
|
+
static decode(token: string, complete?: false): JwtPayload;
|
|
255
|
+
static decode(token: string, complete: true): JwtDecoded;
|
|
256
|
+
static decode(token: string, complete: boolean = false): JwtPayload | JwtDecoded {
|
|
257
|
+
if (!token || typeof token !== 'string') {
|
|
258
|
+
throw new Error('Token必须是非空字符串');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const parts = token.split('.');
|
|
262
|
+
if (parts.length !== 3) {
|
|
263
|
+
throw new Error('JWT格式无效');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const header = JSON.parse(Jwt.base64UrlDecode(parts[0])) as JwtHeader;
|
|
268
|
+
const payload = JSON.parse(Jwt.base64UrlDecode(parts[1])) as JwtPayload;
|
|
269
|
+
|
|
270
|
+
return complete ? { header, payload, signature: parts[2] } : payload;
|
|
271
|
+
} catch (error: any) {
|
|
272
|
+
throw new Error('JWT解码失败: ' + error.message);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 获取 token 剩余有效时间
|
|
278
|
+
* @param token - JWT token字符串
|
|
279
|
+
* @returns 剩余秒数,-1表示已过期或无过期时间
|
|
280
|
+
*/
|
|
281
|
+
static getTimeToExpiry(token: string): number {
|
|
282
|
+
try {
|
|
283
|
+
const payload = this.decode(token);
|
|
284
|
+
if (!payload.exp) return -1;
|
|
285
|
+
const remaining = payload.exp - Math.floor(Date.now() / 1000);
|
|
286
|
+
return remaining > 0 ? remaining : -1;
|
|
287
|
+
} catch {
|
|
288
|
+
return -1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 检查 token 是否已过期
|
|
294
|
+
* @param token - JWT token字符串
|
|
295
|
+
* @returns 是否已过期
|
|
296
|
+
*/
|
|
297
|
+
static isExpired(token: string): boolean {
|
|
298
|
+
return this.getTimeToExpiry(token) <= 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ==================== 应用层便捷方法 ====================
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 签名用户认证 token
|
|
305
|
+
*/
|
|
306
|
+
static signUserToken(userInfo: JwtPayload, options: JwtSignOptions = {}): string {
|
|
307
|
+
return this.sign(userInfo, options);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 签名 API 访问 token
|
|
312
|
+
*/
|
|
313
|
+
static signAPIToken(payload: JwtPayload, options: JwtSignOptions = {}): string {
|
|
314
|
+
return this.sign(payload, { audience: 'api', expiresIn: '1h', ...options });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 签名刷新 token
|
|
319
|
+
*/
|
|
320
|
+
static signRefreshToken(payload: JwtPayload, options: JwtSignOptions = {}): string {
|
|
321
|
+
return this.sign(payload, { audience: 'refresh', expiresIn: '30d', ...options });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 签名临时 token (用于重置密码等)
|
|
326
|
+
*/
|
|
327
|
+
static signTempToken(payload: JwtPayload, options: JwtSignOptions = {}): string {
|
|
328
|
+
return this.sign(payload, { audience: 'temporary', expiresIn: '15m', ...options });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 验证用户认证 token
|
|
333
|
+
*/
|
|
334
|
+
static verifyUserToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
|
|
335
|
+
return this.verify(token, options);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 验证 API 访问 token
|
|
340
|
+
*/
|
|
341
|
+
static verifyAPIToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
|
|
342
|
+
return this.verify(token, { audience: 'api', ...options });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 验证刷新 token
|
|
347
|
+
*/
|
|
348
|
+
static verifyRefreshToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
|
|
349
|
+
return this.verify(token, { audience: 'refresh', ...options });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 验证临时 token
|
|
354
|
+
*/
|
|
355
|
+
static verifyTempToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
|
|
356
|
+
return this.verify(token, { audience: 'temporary', ...options });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 验证 token 并检查权限
|
|
361
|
+
*/
|
|
362
|
+
static verifyWithPermissions(token: string, requiredPermissions: string | string[], options: JwtVerifyOptions = {}): JwtPayload {
|
|
363
|
+
const payload = this.verify(token, options);
|
|
364
|
+
|
|
365
|
+
if (!payload.permissions) {
|
|
366
|
+
throw new Error('Token中不包含权限信息');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const permissions = Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions];
|
|
370
|
+
const hasPermission = permissions.every((permission) => payload.permissions.includes(permission));
|
|
371
|
+
|
|
372
|
+
if (!hasPermission) {
|
|
373
|
+
throw new Error('权限不足');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return payload;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 验证 token 并检查角色
|
|
381
|
+
*/
|
|
382
|
+
static verifyWithRoles(token: string, requiredRoles: string | string[], options: JwtVerifyOptions = {}): JwtPayload {
|
|
383
|
+
const payload = this.verify(token, options);
|
|
384
|
+
|
|
385
|
+
if (!payload.role && !payload.roles) {
|
|
386
|
+
throw new Error('Token中不包含角色信息');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const userRoles = payload.roles || [payload.role];
|
|
390
|
+
const roles = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
|
|
391
|
+
|
|
392
|
+
const hasRole = roles.some((role) => userRoles.includes(role));
|
|
393
|
+
|
|
394
|
+
if (!hasRole) {
|
|
395
|
+
throw new Error('角色权限不足');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return payload;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 软验证 token (忽略过期时间)
|
|
403
|
+
*/
|
|
404
|
+
static verifySoft(token: string, options: JwtVerifyOptions = {}): JwtPayload {
|
|
405
|
+
return this.verify(token, { ignoreExpiration: true, ...options });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 检查 token 是否即将过期
|
|
410
|
+
* @param token - JWT token字符串
|
|
411
|
+
* @param thresholdSeconds - 过期阈值(秒),默认300秒(5分钟)
|
|
412
|
+
* @returns 是否即将过期
|
|
413
|
+
*/
|
|
414
|
+
static isNearExpiry(token: string, thresholdSeconds: number = 300): boolean {
|
|
415
|
+
const timeToExpiry = this.getTimeToExpiry(token);
|
|
416
|
+
return timeToExpiry > 0 && timeToExpiry <= thresholdSeconds;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ==================== 便捷静态方法 ====================
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 创建 token(使用默认配置)
|
|
423
|
+
* @param payload - 载荷数据
|
|
424
|
+
* @returns JWT token字符串
|
|
425
|
+
*/
|
|
426
|
+
static create(payload: JwtPayload): string {
|
|
427
|
+
return this.sign(payload, Env.JWT_SECRET, Env.JWT_EXPIRES_IN || '7d');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 验证 token(使用默认配置)
|
|
432
|
+
* @param token - JWT token字符串
|
|
433
|
+
* @returns 解码后的载荷数据
|
|
434
|
+
*/
|
|
435
|
+
static check(token: string): JwtPayload {
|
|
436
|
+
return this.verify(token, { secret: Env.JWT_SECRET });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 解析 token(不验证签名,使用 decode)
|
|
441
|
+
* @param token - JWT token字符串
|
|
442
|
+
* @returns 解码后的载荷数据
|
|
443
|
+
*/
|
|
444
|
+
static parse(token: string): JwtPayload {
|
|
445
|
+
return this.decode(token);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 检查载荷是否包含特定角色
|
|
450
|
+
* @param payload - JWT载荷
|
|
451
|
+
* @param role - 要检查的角色
|
|
452
|
+
* @returns 是否包含该角色
|
|
453
|
+
*/
|
|
454
|
+
static hasRole(payload: JwtPayload, role: string): boolean {
|
|
455
|
+
if (!payload) return false;
|
|
456
|
+
if (payload.role === role) return true;
|
|
457
|
+
if (Array.isArray(payload.roles) && payload.roles.includes(role)) return true;
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 检查载荷是否包含任意一个角色
|
|
463
|
+
* @param payload - JWT载荷
|
|
464
|
+
* @param roles - 要检查的角色列表
|
|
465
|
+
* @returns 是否包含任意一个角色
|
|
466
|
+
*/
|
|
467
|
+
static hasAnyRole(payload: JwtPayload, roles: string[]): boolean {
|
|
468
|
+
if (!payload) return false;
|
|
469
|
+
return roles.some((role) => this.hasRole(payload, role));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 检查载荷是否包含特定权限
|
|
474
|
+
* @param payload - JWT载荷
|
|
475
|
+
* @param permission - 要检查的权限
|
|
476
|
+
* @returns 是否包含该权限
|
|
477
|
+
*/
|
|
478
|
+
static hasPermission(payload: JwtPayload, permission: string): boolean {
|
|
479
|
+
if (!payload || !payload.permissions) return false;
|
|
480
|
+
return Array.isArray(payload.permissions) && payload.permissions.includes(permission);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 检查载荷是否包含所有权限
|
|
485
|
+
* @param payload - JWT载荷
|
|
486
|
+
* @param permissions - 要检查的权限列表
|
|
487
|
+
* @returns 是否包含所有权限
|
|
488
|
+
*/
|
|
489
|
+
static hasAllPermissions(payload: JwtPayload, permissions: string[]): boolean {
|
|
490
|
+
if (!payload || !payload.permissions) return false;
|
|
491
|
+
return permissions.every((permission) => this.hasPermission(payload, permission));
|
|
492
|
+
}
|
|
493
|
+
}
|