feishu-mcp 0.1.2 → 0.1.3
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/README.md +18 -62
- package/dist/mcp/feishuMcp.js +1 -1
- package/dist/server.js +42 -22
- package/dist/services/baseService.js +121 -37
- package/dist/services/callbackService.js +37 -22
- package/dist/services/feishuApiService.js +140 -19
- package/dist/services/feishuAuthService.js +0 -137
- package/dist/utils/auth/authUtils.js +71 -0
- package/dist/utils/auth/index.js +4 -0
- package/dist/utils/auth/tokenCacheManager.js +420 -0
- package/dist/utils/auth/userAuthManager.js +104 -0
- package/dist/utils/auth/userContextManager.js +81 -0
- package/dist/utils/cache.js +0 -101
- package/dist/utils/error.js +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Logger } from '../logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Token缓存管理器
|
|
6
|
+
* 专门处理用户token和租户token的缓存管理
|
|
7
|
+
*/
|
|
8
|
+
export class TokenCacheManager {
|
|
9
|
+
/**
|
|
10
|
+
* 私有构造函数,用于单例模式
|
|
11
|
+
*/
|
|
12
|
+
constructor() {
|
|
13
|
+
Object.defineProperty(this, "cache", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: void 0
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "userTokenCacheFile", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "tenantTokenCacheFile", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: void 0
|
|
30
|
+
});
|
|
31
|
+
this.cache = new Map();
|
|
32
|
+
this.userTokenCacheFile = path.resolve(process.cwd(), 'user_token_cache.json');
|
|
33
|
+
this.tenantTokenCacheFile = path.resolve(process.cwd(), 'tenant_token_cache.json');
|
|
34
|
+
this.loadTokenCaches();
|
|
35
|
+
this.startCacheCleanupTimer();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 获取TokenCacheManager实例
|
|
39
|
+
*/
|
|
40
|
+
static getInstance() {
|
|
41
|
+
if (!TokenCacheManager.instance) {
|
|
42
|
+
TokenCacheManager.instance = new TokenCacheManager();
|
|
43
|
+
}
|
|
44
|
+
return TokenCacheManager.instance;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 系统启动时从本地文件缓存中读取token记录
|
|
48
|
+
*/
|
|
49
|
+
loadTokenCaches() {
|
|
50
|
+
this.loadUserTokenCache();
|
|
51
|
+
this.loadTenantTokenCache();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 加载用户token缓存
|
|
55
|
+
*/
|
|
56
|
+
loadUserTokenCache() {
|
|
57
|
+
if (fs.existsSync(this.userTokenCacheFile)) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(this.userTokenCacheFile, 'utf-8');
|
|
60
|
+
const cacheData = JSON.parse(raw);
|
|
61
|
+
let loadedCount = 0;
|
|
62
|
+
for (const key in cacheData) {
|
|
63
|
+
if (key.startsWith('user_access_token:')) {
|
|
64
|
+
this.cache.set(key, cacheData[key]);
|
|
65
|
+
loadedCount++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
Logger.info(`已加载用户token缓存,共 ${loadedCount} 条记录`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
Logger.warn('加载用户token缓存失败:', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
Logger.info('用户token缓存文件不存在,将创建新的缓存');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 加载租户token缓存
|
|
80
|
+
*/
|
|
81
|
+
loadTenantTokenCache() {
|
|
82
|
+
if (fs.existsSync(this.tenantTokenCacheFile)) {
|
|
83
|
+
try {
|
|
84
|
+
const raw = fs.readFileSync(this.tenantTokenCacheFile, 'utf-8');
|
|
85
|
+
const cacheData = JSON.parse(raw);
|
|
86
|
+
let loadedCount = 0;
|
|
87
|
+
for (const key in cacheData) {
|
|
88
|
+
if (key.startsWith('tenant_access_token:')) {
|
|
89
|
+
this.cache.set(key, cacheData[key]);
|
|
90
|
+
loadedCount++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
Logger.info(`已加载租户token缓存,共 ${loadedCount} 条记录`);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
Logger.warn('加载租户token缓存失败:', error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 根据key获取完整的用户token信息
|
|
102
|
+
* @param key 缓存键
|
|
103
|
+
* @returns 完整的用户token信息对象,如果未找到或refresh_token过期则返回null
|
|
104
|
+
*/
|
|
105
|
+
getUserTokenInfo(key) {
|
|
106
|
+
const cacheKey = `user_access_token:${key}`;
|
|
107
|
+
const cacheItem = this.cache.get(cacheKey);
|
|
108
|
+
if (!cacheItem) {
|
|
109
|
+
Logger.debug(`用户token信息未找到: ${key}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const tokenInfo = cacheItem.data;
|
|
113
|
+
const now = Math.floor(Date.now() / 1000);
|
|
114
|
+
// 检查refresh_token是否过期(如果有的话)
|
|
115
|
+
if (tokenInfo.refresh_token && tokenInfo.refresh_token_expires_at) {
|
|
116
|
+
if (tokenInfo.refresh_token_expires_at < now) {
|
|
117
|
+
Logger.debug(`用户token的refresh_token已过期,从缓存中删除: ${key}`);
|
|
118
|
+
this.cache.delete(cacheKey);
|
|
119
|
+
this.saveUserTokenCache();
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// 如果没有refresh_token信息,检查缓存本身是否过期
|
|
125
|
+
if (Date.now() > cacheItem.expiresAt) {
|
|
126
|
+
Logger.debug(`用户token缓存已过期: ${key}`);
|
|
127
|
+
this.cache.delete(cacheKey);
|
|
128
|
+
this.saveUserTokenCache();
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
Logger.debug(`获取用户token信息成功: ${key}`);
|
|
133
|
+
return tokenInfo;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 根据key获取用户的access_token值
|
|
137
|
+
* @param key 缓存键
|
|
138
|
+
* @returns access_token字符串,如果未找到或已过期则返回null
|
|
139
|
+
*/
|
|
140
|
+
getUserToken(key) {
|
|
141
|
+
const tokenInfo = this.getUserTokenInfo(key);
|
|
142
|
+
return tokenInfo ? tokenInfo.access_token : null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 根据key获取租户token信息
|
|
146
|
+
* @param key 缓存键
|
|
147
|
+
* @returns 租户token信息,如果未找到或已过期则返回null
|
|
148
|
+
*/
|
|
149
|
+
getTenantTokenInfo(key) {
|
|
150
|
+
const cacheKey = `tenant_access_token:${key}`;
|
|
151
|
+
const cacheItem = this.cache.get(cacheKey);
|
|
152
|
+
if (!cacheItem) {
|
|
153
|
+
Logger.debug(`租户token信息未找到: ${key}`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// 检查是否过期
|
|
157
|
+
if (Date.now() > cacheItem.expiresAt) {
|
|
158
|
+
Logger.debug(`租户token信息已过期: ${key}`);
|
|
159
|
+
this.cache.delete(cacheKey);
|
|
160
|
+
this.saveTenantTokenCache();
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
Logger.debug(`获取租户token信息成功: ${key}`);
|
|
164
|
+
return cacheItem.data;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 删除租户token
|
|
168
|
+
* @param key 缓存键
|
|
169
|
+
* @returns 是否成功删除
|
|
170
|
+
*/
|
|
171
|
+
removeTenantToken(key) {
|
|
172
|
+
const cacheKey = `tenant_access_token:${key}`;
|
|
173
|
+
const result = this.cache.delete(cacheKey);
|
|
174
|
+
if (result) {
|
|
175
|
+
this.saveUserTokenCache();
|
|
176
|
+
Logger.debug(`租户token删除成功: ${key}`);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 根据key获取租户的access_token值
|
|
182
|
+
* @param key 缓存键
|
|
183
|
+
* @returns app_access_token字符串,如果未找到或已过期则返回null
|
|
184
|
+
*/
|
|
185
|
+
getTenantToken(key) {
|
|
186
|
+
const tokenInfo = this.getTenantTokenInfo(key);
|
|
187
|
+
return tokenInfo ? tokenInfo.app_access_token : null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 缓存用户token信息
|
|
191
|
+
* @param key 缓存键
|
|
192
|
+
* @param tokenInfo 用户token信息
|
|
193
|
+
* @param customTtl 自定义TTL(秒),如果不提供则使用refresh_token的过期时间
|
|
194
|
+
* @returns 是否成功缓存
|
|
195
|
+
*/
|
|
196
|
+
cacheUserToken(key, tokenInfo, customTtl) {
|
|
197
|
+
try {
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const cacheKey = `user_access_token:${key}`;
|
|
200
|
+
// 计算过期时间 - 优先使用refresh_token的过期时间,确保可以刷新
|
|
201
|
+
let expiresAt;
|
|
202
|
+
if (customTtl) {
|
|
203
|
+
expiresAt = now + (customTtl * 1000);
|
|
204
|
+
}
|
|
205
|
+
else if (tokenInfo.refresh_token_expires_at) {
|
|
206
|
+
// 使用refresh_token的过期时间,确保在refresh_token有效期内缓存不会被清除
|
|
207
|
+
expiresAt = tokenInfo.refresh_token_expires_at * 1000; // 转换为毫秒
|
|
208
|
+
Logger.debug(`使用refresh_token过期时间作为缓存过期时间: ${new Date(expiresAt).toISOString()}`);
|
|
209
|
+
}
|
|
210
|
+
else if (tokenInfo.expires_at) {
|
|
211
|
+
// 如果没有refresh_token_expires_at信息,降级使用access_token的过期时间
|
|
212
|
+
expiresAt = tokenInfo.expires_at * 1000;
|
|
213
|
+
Logger.warn(`没有refresh_token过期时间戳,使用access_token过期时间: ${new Date(expiresAt).toISOString()}`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// 最后的降级方案:如果没有任何过期时间信息,设置默认的2小时过期
|
|
217
|
+
expiresAt = now + (2 * 60 * 60 * 1000); // 2小时
|
|
218
|
+
Logger.warn(`没有过期时间信息,使用默认2小时作为缓存过期时间`);
|
|
219
|
+
}
|
|
220
|
+
const cacheItem = {
|
|
221
|
+
data: tokenInfo,
|
|
222
|
+
timestamp: now,
|
|
223
|
+
expiresAt: expiresAt
|
|
224
|
+
};
|
|
225
|
+
this.cache.set(cacheKey, cacheItem);
|
|
226
|
+
this.saveUserTokenCache();
|
|
227
|
+
Logger.debug(`用户token缓存成功: ${key}, 缓存过期时间: ${new Date(expiresAt).toISOString()}`);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
Logger.error(`缓存用户token失败: ${key}`, error);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 缓存租户token信息
|
|
237
|
+
* @param key 缓存键
|
|
238
|
+
* @param tokenInfo 租户token信息
|
|
239
|
+
* @param customTtl 自定义TTL(秒),如果不提供则使用token本身的过期时间
|
|
240
|
+
* @returns 是否成功缓存
|
|
241
|
+
*/
|
|
242
|
+
cacheTenantToken(key, tokenInfo, customTtl) {
|
|
243
|
+
try {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const cacheKey = `tenant_access_token:${key}`;
|
|
246
|
+
// 计算过期时间
|
|
247
|
+
let expiresAt;
|
|
248
|
+
if (customTtl) {
|
|
249
|
+
expiresAt = now + (customTtl * 1000);
|
|
250
|
+
}
|
|
251
|
+
else if (tokenInfo.expires_at) {
|
|
252
|
+
expiresAt = tokenInfo.expires_at * 1000; // 转换为毫秒
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// 如果没有过期时间信息,设置默认的2小时过期
|
|
256
|
+
expiresAt = now + (2 * 60 * 60 * 1000);
|
|
257
|
+
Logger.warn(`租户token没有过期时间信息,使用默认2小时`);
|
|
258
|
+
}
|
|
259
|
+
const cacheItem = {
|
|
260
|
+
data: tokenInfo,
|
|
261
|
+
timestamp: now,
|
|
262
|
+
expiresAt: expiresAt
|
|
263
|
+
};
|
|
264
|
+
this.cache.set(cacheKey, cacheItem);
|
|
265
|
+
this.saveTenantTokenCache();
|
|
266
|
+
Logger.debug(`租户token缓存成功: ${key}, 过期时间: ${new Date(expiresAt).toISOString()}`);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
Logger.error(`缓存租户token失败: ${key}`, error);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 检查用户token状态
|
|
276
|
+
* @param key 缓存键
|
|
277
|
+
* @returns token状态信息
|
|
278
|
+
*/
|
|
279
|
+
checkUserTokenStatus(key) {
|
|
280
|
+
const tokenInfo = this.getUserTokenInfo(key);
|
|
281
|
+
if (!tokenInfo) {
|
|
282
|
+
return {
|
|
283
|
+
isValid: false,
|
|
284
|
+
isExpired: true,
|
|
285
|
+
canRefresh: false,
|
|
286
|
+
shouldRefresh: false
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const now = Math.floor(Date.now() / 1000);
|
|
290
|
+
const isExpired = tokenInfo.expires_at ? tokenInfo.expires_at < now : false;
|
|
291
|
+
const timeToExpiry = tokenInfo.expires_at ? Math.max(0, tokenInfo.expires_at - now) : 0;
|
|
292
|
+
// 判断是否可以刷新
|
|
293
|
+
const canRefresh = !!(tokenInfo.refresh_token &&
|
|
294
|
+
tokenInfo.refresh_token_expires_at &&
|
|
295
|
+
tokenInfo.refresh_token_expires_at > now);
|
|
296
|
+
// 判断是否应该提前刷新(提前5分钟)
|
|
297
|
+
const shouldRefresh = timeToExpiry > 0 && timeToExpiry < 300 && canRefresh;
|
|
298
|
+
return {
|
|
299
|
+
isValid: !isExpired,
|
|
300
|
+
isExpired,
|
|
301
|
+
canRefresh,
|
|
302
|
+
shouldRefresh
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 删除用户token
|
|
307
|
+
* @param key 缓存键
|
|
308
|
+
* @returns 是否成功删除
|
|
309
|
+
*/
|
|
310
|
+
removeUserToken(key) {
|
|
311
|
+
const cacheKey = `user_access_token:${key}`;
|
|
312
|
+
const result = this.cache.delete(cacheKey);
|
|
313
|
+
if (result) {
|
|
314
|
+
this.saveUserTokenCache();
|
|
315
|
+
Logger.debug(`用户token删除成功: ${key}`);
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* 保存用户token缓存到文件
|
|
321
|
+
*/
|
|
322
|
+
saveUserTokenCache() {
|
|
323
|
+
const cacheData = {};
|
|
324
|
+
for (const [key, value] of this.cache.entries()) {
|
|
325
|
+
if (key.startsWith('user_access_token:')) {
|
|
326
|
+
cacheData[key] = value;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
fs.writeFileSync(this.userTokenCacheFile, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
331
|
+
Logger.debug('用户token缓存已保存到文件');
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
Logger.warn('保存用户token缓存失败:', error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 保存租户token缓存到文件
|
|
339
|
+
*/
|
|
340
|
+
saveTenantTokenCache() {
|
|
341
|
+
const cacheData = {};
|
|
342
|
+
for (const [key, value] of this.cache.entries()) {
|
|
343
|
+
if (key.startsWith('tenant_access_token:')) {
|
|
344
|
+
cacheData[key] = value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
fs.writeFileSync(this.tenantTokenCacheFile, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
349
|
+
Logger.debug('租户token缓存已保存到文件');
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
Logger.warn('保存租户token缓存失败:', error);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 清理过期缓存
|
|
357
|
+
* 对于用户token,只有在refresh_token过期时才清理
|
|
358
|
+
* 对于租户token,按缓存过期时间清理
|
|
359
|
+
* @returns 清理的数量
|
|
360
|
+
*/
|
|
361
|
+
cleanExpiredTokens() {
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const nowSeconds = Math.floor(now / 1000);
|
|
364
|
+
let cleanedCount = 0;
|
|
365
|
+
const keysToDelete = [];
|
|
366
|
+
for (const [key, cacheItem] of this.cache.entries()) {
|
|
367
|
+
let shouldDelete = false;
|
|
368
|
+
if (key.startsWith('user_access_token:')) {
|
|
369
|
+
// 用户token:检查refresh_token是否过期
|
|
370
|
+
const tokenInfo = cacheItem.data;
|
|
371
|
+
if (tokenInfo.refresh_token && tokenInfo.refresh_token_expires_at) {
|
|
372
|
+
// 有refresh_token,只有refresh_token过期才删除
|
|
373
|
+
shouldDelete = tokenInfo.refresh_token_expires_at < nowSeconds;
|
|
374
|
+
if (shouldDelete) {
|
|
375
|
+
Logger.debug(`清理用户token - refresh_token已过期: ${key}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// 没有refresh_token,按缓存过期时间删除
|
|
380
|
+
shouldDelete = cacheItem.expiresAt <= now;
|
|
381
|
+
if (shouldDelete) {
|
|
382
|
+
Logger.debug(`清理用户token - 无refresh_token且缓存过期: ${key}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// 租户token或其他类型:按缓存过期时间删除
|
|
388
|
+
shouldDelete = cacheItem.expiresAt <= now;
|
|
389
|
+
if (shouldDelete) {
|
|
390
|
+
Logger.debug(`清理过期缓存: ${key}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (shouldDelete) {
|
|
394
|
+
keysToDelete.push(key);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// 批量删除
|
|
398
|
+
keysToDelete.forEach(key => {
|
|
399
|
+
this.cache.delete(key);
|
|
400
|
+
cleanedCount++;
|
|
401
|
+
});
|
|
402
|
+
if (cleanedCount > 0) {
|
|
403
|
+
// 分别保存用户和租户缓存
|
|
404
|
+
this.saveUserTokenCache();
|
|
405
|
+
this.saveTenantTokenCache();
|
|
406
|
+
Logger.info(`清理过期token,删除了 ${cleanedCount} 条记录`);
|
|
407
|
+
}
|
|
408
|
+
return cleanedCount;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 启动缓存清理定时器
|
|
412
|
+
*/
|
|
413
|
+
startCacheCleanupTimer() {
|
|
414
|
+
// 每5分钟清理一次过期缓存
|
|
415
|
+
setInterval(() => {
|
|
416
|
+
this.cleanExpiredTokens();
|
|
417
|
+
}, 5 * 60 * 1000);
|
|
418
|
+
Logger.info('Token缓存清理定时器已启动,每5分钟执行一次');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Logger } from '../logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* 用户认证管理器
|
|
4
|
+
* 管理 sessionId 与 userKey 的映射关系
|
|
5
|
+
*/
|
|
6
|
+
export class UserAuthManager {
|
|
7
|
+
/**
|
|
8
|
+
* 私有构造函数,用于单例模式
|
|
9
|
+
*/
|
|
10
|
+
constructor() {
|
|
11
|
+
Object.defineProperty(this, "sessionToUserKey", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
}); // sessionId -> userKey
|
|
17
|
+
this.sessionToUserKey = new Map();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 获取用户认证管理器实例
|
|
21
|
+
* @returns 用户认证管理器实例
|
|
22
|
+
*/
|
|
23
|
+
static getInstance() {
|
|
24
|
+
if (!UserAuthManager.instance) {
|
|
25
|
+
UserAuthManager.instance = new UserAuthManager();
|
|
26
|
+
}
|
|
27
|
+
return UserAuthManager.instance;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 创建用户会话
|
|
31
|
+
* @param sessionId 会话ID
|
|
32
|
+
* @param userKey 用户密钥
|
|
33
|
+
* @returns 是否创建成功
|
|
34
|
+
*/
|
|
35
|
+
createSession(sessionId, userKey) {
|
|
36
|
+
if (!sessionId || !userKey) {
|
|
37
|
+
Logger.warn('创建会话失败:sessionId 或 userKey 为空');
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
this.sessionToUserKey.set(sessionId, userKey);
|
|
41
|
+
Logger.info(`创建用户会话:sessionId=${sessionId}, userKey=${userKey}`);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 根据 sessionId 获取 userKey
|
|
46
|
+
* @param sessionId 会话ID
|
|
47
|
+
* @returns 用户密钥,如果未找到则返回 null
|
|
48
|
+
*/
|
|
49
|
+
getUserKeyBySessionId(sessionId) {
|
|
50
|
+
if (!sessionId) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const userKey = this.sessionToUserKey.get(sessionId);
|
|
54
|
+
if (!userKey) {
|
|
55
|
+
Logger.debug(`未找到会话:${sessionId}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
Logger.debug(`获取用户密钥:sessionId=${sessionId}, userKey=${userKey}`);
|
|
59
|
+
return userKey;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 删除会话
|
|
63
|
+
* @param sessionId 会话ID
|
|
64
|
+
* @returns 是否删除成功
|
|
65
|
+
*/
|
|
66
|
+
removeSession(sessionId) {
|
|
67
|
+
if (!sessionId) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const userKey = this.sessionToUserKey.get(sessionId);
|
|
71
|
+
if (!userKey) {
|
|
72
|
+
Logger.debug(`会话不存在:${sessionId}`);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
this.sessionToUserKey.delete(sessionId);
|
|
76
|
+
Logger.info(`删除用户会话:sessionId=${sessionId}, userKey=${userKey}`);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 检查会话是否存在
|
|
81
|
+
* @param sessionId 会话ID
|
|
82
|
+
* @returns 会话是否存在
|
|
83
|
+
*/
|
|
84
|
+
hasSession(sessionId) {
|
|
85
|
+
return this.sessionToUserKey.has(sessionId);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 获取所有会话统计信息
|
|
89
|
+
* @returns 会话统计信息
|
|
90
|
+
*/
|
|
91
|
+
getStats() {
|
|
92
|
+
return {
|
|
93
|
+
totalSessions: this.sessionToUserKey.size
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 清空所有会话
|
|
98
|
+
*/
|
|
99
|
+
clearAllSessions() {
|
|
100
|
+
const count = this.sessionToUserKey.size;
|
|
101
|
+
this.sessionToUserKey.clear();
|
|
102
|
+
Logger.info(`清空所有会话,删除了 ${count} 个会话`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
/**
|
|
3
|
+
* 用户上下文管理器
|
|
4
|
+
* 使用 AsyncLocalStorage 在异步调用链中传递用户信息
|
|
5
|
+
*/
|
|
6
|
+
export class UserContextManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
Object.defineProperty(this, "asyncLocalStorage", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
this.asyncLocalStorage = new AsyncLocalStorage();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 获取单例实例
|
|
18
|
+
*/
|
|
19
|
+
static getInstance() {
|
|
20
|
+
if (!UserContextManager.instance) {
|
|
21
|
+
UserContextManager.instance = new UserContextManager();
|
|
22
|
+
}
|
|
23
|
+
return UserContextManager.instance;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 在指定上下文中运行回调函数
|
|
27
|
+
* @param context 用户上下文
|
|
28
|
+
* @param callback 回调函数
|
|
29
|
+
* @returns 回调函数的返回值
|
|
30
|
+
*/
|
|
31
|
+
run(context, callback) {
|
|
32
|
+
return this.asyncLocalStorage.run(context, callback);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 获取当前上下文中的用户密钥
|
|
36
|
+
* @returns 用户密钥,如果不存在则返回空字符串
|
|
37
|
+
*/
|
|
38
|
+
getUserKey() {
|
|
39
|
+
const context = this.asyncLocalStorage.getStore();
|
|
40
|
+
return context?.userKey || '';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 获取当前上下文中的基础URL
|
|
44
|
+
* @returns 基础URL,如果不存在则返回空字符串
|
|
45
|
+
*/
|
|
46
|
+
getBaseUrl() {
|
|
47
|
+
const context = this.asyncLocalStorage.getStore();
|
|
48
|
+
return context?.baseUrl || '';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 获取当前完整的用户上下文
|
|
52
|
+
* @returns 用户上下文,如果不存在则返回 undefined
|
|
53
|
+
*/
|
|
54
|
+
getContext() {
|
|
55
|
+
return this.asyncLocalStorage.getStore();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 检查是否存在用户上下文
|
|
59
|
+
* @returns 如果存在用户上下文则返回 true
|
|
60
|
+
*/
|
|
61
|
+
hasContext() {
|
|
62
|
+
return this.asyncLocalStorage.getStore() !== undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 获取协议
|
|
67
|
+
*/
|
|
68
|
+
function getProtocol(req) {
|
|
69
|
+
if (req.secure || req.get('X-Forwarded-Proto') === 'https') {
|
|
70
|
+
return 'https';
|
|
71
|
+
}
|
|
72
|
+
return 'http';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 获取基础URL
|
|
76
|
+
*/
|
|
77
|
+
export function getBaseUrl(req) {
|
|
78
|
+
const protocol = getProtocol(req);
|
|
79
|
+
const host = req.get('X-Forwarded-Host') || req.get('host');
|
|
80
|
+
return `${protocol}://${host}`;
|
|
81
|
+
}
|