@be-link/http 1.0.1-beta.0
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 +425 -0
- package/dist/BeLinkHttp.d.ts +206 -0
- package/dist/BeLinkHttp.d.ts.map +1 -0
- package/dist/index.cjs.js +845 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +835 -0
- package/dist/interceptors/index.d.ts +10 -0
- package/dist/interceptors/index.d.ts.map +1 -0
- package/dist/interceptors/request.d.ts +50 -0
- package/dist/interceptors/request.d.ts.map +1 -0
- package/dist/interceptors/response.d.ts +52 -0
- package/dist/interceptors/response.d.ts.map +1 -0
- package/dist/services/EncryptionService.d.ts +120 -0
- package/dist/services/EncryptionService.d.ts.map +1 -0
- package/dist/services/TimeSyncService.d.ts +117 -0
- package/dist/services/TimeSyncService.d.ts.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var axios = require('axios');
|
|
6
|
+
var CryptoJS = require('crypto-js');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 时间同步服务
|
|
10
|
+
*
|
|
11
|
+
* 用于同步客户端和服务器时间,解决客户端时间不准确的问题
|
|
12
|
+
* 主要用于 Token 加密时使用服务器时间,防止重放攻击
|
|
13
|
+
*
|
|
14
|
+
* 使用 performance.now() 计算时间流逝,防止用户修改系统时间导致的问题
|
|
15
|
+
*
|
|
16
|
+
* @module TimeSyncService
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* localStorage 存储键名常量
|
|
20
|
+
*/
|
|
21
|
+
const STORAGE_KEYS = {
|
|
22
|
+
/** 服务器时间存储键 */
|
|
23
|
+
SERVER_TIME: 'belink_server_time',
|
|
24
|
+
/** 客户端时间存储键(同步时的客户端时间,用于判断是否需要重新同步) */
|
|
25
|
+
CLIENT_TIME: 'belink_client_time',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* 检测是否为浏览器环境
|
|
29
|
+
*/
|
|
30
|
+
function isBrowser() {
|
|
31
|
+
return typeof window !== 'undefined' && typeof localStorage !== 'undefined';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 获取带前缀的存储 key
|
|
35
|
+
*/
|
|
36
|
+
function getStorageKey(prefix, key) {
|
|
37
|
+
return prefix ? `${prefix}_${key}` : key;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 获取 performance.now(),兼容非浏览器环境
|
|
41
|
+
*/
|
|
42
|
+
function getPerformanceNow() {
|
|
43
|
+
if (typeof performance !== 'undefined' && performance.now) {
|
|
44
|
+
return performance.now();
|
|
45
|
+
}
|
|
46
|
+
return Date.now();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 时间同步服务
|
|
50
|
+
*
|
|
51
|
+
* 负责同步客户端和服务器时间,提供以下功能:
|
|
52
|
+
* - 自动检测是否需要同步
|
|
53
|
+
* - 获取服务器时间
|
|
54
|
+
* - 使用 performance.now() 计算时间流逝,防止用户修改系统时间
|
|
55
|
+
* - 获取调整后的当前时间
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const timeSync = new TimeSyncService({
|
|
60
|
+
* syncUrl: 'https://api.example.com/api/time',
|
|
61
|
+
* syncGapTime: 50000,
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // 确保时间同步
|
|
65
|
+
* await timeSync.ensureSync();
|
|
66
|
+
*
|
|
67
|
+
* // 获取调整后的时间
|
|
68
|
+
* const adjustedTime = timeSync.getAdjustedTime();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
class TimeSyncService {
|
|
72
|
+
/**
|
|
73
|
+
* 创建时间同步服务实例
|
|
74
|
+
*/
|
|
75
|
+
constructor(config) {
|
|
76
|
+
/**
|
|
77
|
+
* 同步时的服务器时间(内存存储)
|
|
78
|
+
* 用于计算调整后的时间
|
|
79
|
+
*/
|
|
80
|
+
this.syncServerTime = null;
|
|
81
|
+
/**
|
|
82
|
+
* 同步时的 performance.now() 值(内存存储)
|
|
83
|
+
* 使用 performance.now() 而不是 Date.now(),防止用户修改系统时间
|
|
84
|
+
*/
|
|
85
|
+
this.syncPerformanceTime = null;
|
|
86
|
+
/**
|
|
87
|
+
* 正在进行的同步 Promise(用于防止并发请求时重复同步)
|
|
88
|
+
* 当有同步请求正在进行时,其他请求会等待这个 Promise 完成
|
|
89
|
+
*/
|
|
90
|
+
this.syncPromise = null;
|
|
91
|
+
this.config = {
|
|
92
|
+
enabled: true,
|
|
93
|
+
syncGapTime: 50 * 1000, // 默认 50 秒
|
|
94
|
+
...config,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 确保时间已同步
|
|
99
|
+
*
|
|
100
|
+
* 检查是否需要同步:
|
|
101
|
+
* 1. 内存中没有同步数据(页面刷新后)
|
|
102
|
+
* 2. 距离上次同步的时间超过了 syncGapTime 阈值
|
|
103
|
+
*
|
|
104
|
+
* 并发控制:
|
|
105
|
+
* - 如果已有同步请求正在进行,直接等待该请求完成
|
|
106
|
+
* - 避免多个并发请求同时触发多次时间同步
|
|
107
|
+
*/
|
|
108
|
+
async ensureSync() {
|
|
109
|
+
if (!this.config.enabled || !isBrowser()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// 如果已有同步请求正在进行,等待它完成
|
|
113
|
+
if (this.syncPromise) {
|
|
114
|
+
await this.syncPromise;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// 如果内存中没有同步数据,需要同步
|
|
118
|
+
if (this.syncServerTime === null || this.syncPerformanceTime === null) {
|
|
119
|
+
await this.sync();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// 使用 performance.now() 计算时间流逝,检查是否需要重新同步
|
|
123
|
+
const elapsed = getPerformanceNow() - this.syncPerformanceTime;
|
|
124
|
+
if (elapsed > (this.config.syncGapTime || 50 * 1000)) {
|
|
125
|
+
await this.sync();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 执行时间同步
|
|
130
|
+
*
|
|
131
|
+
* 向服务器发送请求获取服务器时间
|
|
132
|
+
* 同时记录 performance.now() 用于后续计算时间流逝
|
|
133
|
+
*
|
|
134
|
+
* 并发控制:
|
|
135
|
+
* - 使用 syncPromise 作为锁,确保同一时间只有一个同步请求
|
|
136
|
+
* - 同步完成后清除锁,允许后续同步请求
|
|
137
|
+
*/
|
|
138
|
+
async sync() {
|
|
139
|
+
if (!isBrowser() || !this.config.syncUrl) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// 如果已有同步请求正在进行,等待它完成
|
|
143
|
+
if (this.syncPromise) {
|
|
144
|
+
await this.syncPromise;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// 创建同步 Promise 并保存引用(作为锁)
|
|
148
|
+
this.syncPromise = this.doSync();
|
|
149
|
+
try {
|
|
150
|
+
await this.syncPromise;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
// 同步完成后清除锁
|
|
154
|
+
this.syncPromise = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 实际执行时间同步的内部方法
|
|
159
|
+
*/
|
|
160
|
+
async doSync() {
|
|
161
|
+
try {
|
|
162
|
+
// 记录请求发起时的 performance.now()
|
|
163
|
+
const requestStartTime = getPerformanceNow();
|
|
164
|
+
const response = await fetch(this.config.syncUrl, { method: 'POST' });
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
console.error('[TimeSyncService] Sync failed with status:', response.status);
|
|
167
|
+
this.clear();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const res = await response.json();
|
|
171
|
+
const serverTime = res.data.timestamp;
|
|
172
|
+
if (!serverTime) {
|
|
173
|
+
this.clear();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// 计算网络延迟的一半,补偿到服务器时间
|
|
177
|
+
const requestEndTime = getPerformanceNow();
|
|
178
|
+
const networkDelay = (requestEndTime - requestStartTime) / 2;
|
|
179
|
+
// 存储到内存(使用 performance.now())
|
|
180
|
+
this.syncServerTime = serverTime + networkDelay;
|
|
181
|
+
this.syncPerformanceTime = requestEndTime;
|
|
182
|
+
// 同时存储到 localStorage(用于调试和日志)
|
|
183
|
+
const serverTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.SERVER_TIME);
|
|
184
|
+
const clientTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.CLIENT_TIME);
|
|
185
|
+
localStorage.setItem(serverTimeKey, String(this.syncServerTime));
|
|
186
|
+
localStorage.setItem(clientTimeKey, String(Date.now()));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error('[TimeSyncService] Failed to sync time:', error);
|
|
190
|
+
this.clear();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 获取服务器时间
|
|
195
|
+
*
|
|
196
|
+
* 返回同步时获取的服务器时间戳
|
|
197
|
+
*/
|
|
198
|
+
getServerTime() {
|
|
199
|
+
return this.syncServerTime;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 获取时间差
|
|
203
|
+
*
|
|
204
|
+
* 返回服务器时间与客户端时间的差值
|
|
205
|
+
* 注意:此方法仅用于向后兼容,实际计算使用 getAdjustedTime()
|
|
206
|
+
*/
|
|
207
|
+
getTimeDiff() {
|
|
208
|
+
if (this.syncServerTime === null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return this.syncServerTime - Date.now();
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 获取调整后的当前时间
|
|
215
|
+
*
|
|
216
|
+
* 使用 performance.now() 计算时间流逝,得到准确的服务器时间
|
|
217
|
+
* 计算公式:同步时的服务器时间 + (当前 performance.now() - 同步时的 performance.now())
|
|
218
|
+
*
|
|
219
|
+
* 优势:即使用户修改系统时间,performance.now() 的流逝是正确的
|
|
220
|
+
*
|
|
221
|
+
* @returns 调整后的时间戳(毫秒),未同步时返回当前客户端时间
|
|
222
|
+
*/
|
|
223
|
+
getAdjustedTime() {
|
|
224
|
+
if (this.syncServerTime === null || this.syncPerformanceTime === null) {
|
|
225
|
+
return Date.now();
|
|
226
|
+
}
|
|
227
|
+
// 使用 performance.now() 计算时间流逝
|
|
228
|
+
const elapsed = getPerformanceNow() - this.syncPerformanceTime;
|
|
229
|
+
return Math.round(this.syncServerTime + elapsed);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 清除同步数据
|
|
233
|
+
*/
|
|
234
|
+
clear() {
|
|
235
|
+
this.syncServerTime = null;
|
|
236
|
+
this.syncPerformanceTime = null;
|
|
237
|
+
if (isBrowser()) {
|
|
238
|
+
const serverTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.SERVER_TIME);
|
|
239
|
+
const clientTimeKey = getStorageKey(this.config.storagePrefix, STORAGE_KEYS.CLIENT_TIME);
|
|
240
|
+
localStorage.removeItem(serverTimeKey);
|
|
241
|
+
localStorage.removeItem(clientTimeKey);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 更新配置
|
|
246
|
+
*/
|
|
247
|
+
updateConfig(config) {
|
|
248
|
+
this.config = { ...this.config, ...config };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 加密服务
|
|
254
|
+
*
|
|
255
|
+
* 提供 Token 加密/解密功能,使用 AES-CBC 模式
|
|
256
|
+
* 加密后的 Token 包含时间戳,用于防止重放攻击
|
|
257
|
+
*
|
|
258
|
+
* @module EncryptionService
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* 加密服务
|
|
262
|
+
*
|
|
263
|
+
* 负责 Token 的 AES 加密和解密,提供以下功能:
|
|
264
|
+
* - Token 加密(带时间戳)
|
|
265
|
+
* - Token 解密
|
|
266
|
+
* - 与时间同步服务集成
|
|
267
|
+
*
|
|
268
|
+
* 加密格式说明:
|
|
269
|
+
* - 原始数据格式: `${token}|+|${timestamp}`
|
|
270
|
+
* - 加密算法: AES-128-CBC
|
|
271
|
+
* - 输出格式: Base64 编码
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* const encryption = new EncryptionService({
|
|
276
|
+
* key: '1234567890123456', // 16 位
|
|
277
|
+
* iv: '1234567890123456', // 16 位
|
|
278
|
+
* });
|
|
279
|
+
*
|
|
280
|
+
* // 加密 Token
|
|
281
|
+
* const encryptedToken = encryption.encryptToken('my-token');
|
|
282
|
+
*
|
|
283
|
+
* // 解密 Token
|
|
284
|
+
* const { token, timestamp } = encryption.decryptToken(encryptedToken);
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
class EncryptionService {
|
|
288
|
+
/**
|
|
289
|
+
* 创建加密服务实例
|
|
290
|
+
*
|
|
291
|
+
* @param config - 加密配置
|
|
292
|
+
* @param timeSyncService - 时间同步服务实例(可选),用于获取服务器校正后的时间
|
|
293
|
+
*/
|
|
294
|
+
constructor(config, timeSyncService) {
|
|
295
|
+
this.config = {
|
|
296
|
+
enabled: true,
|
|
297
|
+
...config,
|
|
298
|
+
};
|
|
299
|
+
this.timeSyncService = timeSyncService;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* 加密 Token
|
|
303
|
+
*
|
|
304
|
+
* 将 Token 与时间戳组合后进行 AES 加密
|
|
305
|
+
* 加密后的数据用于请求头的 Authorization 字段
|
|
306
|
+
*
|
|
307
|
+
* 加密流程:
|
|
308
|
+
* 1. 组合原始数据: `${token}|+|${timestamp}`
|
|
309
|
+
* 2. 使用 AES-128-CBC 加密
|
|
310
|
+
* 3. 输出 Base64 编码的密文
|
|
311
|
+
*
|
|
312
|
+
* @param token - 原始 Token 字符串
|
|
313
|
+
* @param timestamp - 可选的时间戳(毫秒),默认使用调整后的当前时间
|
|
314
|
+
* @returns 加密后的 Token 字符串(Base64 编码)
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* // 使用默认时间戳
|
|
319
|
+
* const encrypted = encryption.encryptToken('my-token');
|
|
320
|
+
*
|
|
321
|
+
* // 使用指定时间戳
|
|
322
|
+
* const encrypted = encryption.encryptToken('my-token', Date.now());
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
encryptToken(token, timestamp) {
|
|
326
|
+
// 如果 Token 为空或加密功能禁用,直接返回原始 Token
|
|
327
|
+
if (!token || !this.config.enabled) {
|
|
328
|
+
return token || '';
|
|
329
|
+
}
|
|
330
|
+
// 获取时间戳(优先使用传入的时间戳,否则使用调整后的时间)
|
|
331
|
+
const time = timestamp ?? this.getAdjustedTime();
|
|
332
|
+
// 组合原始数据:Token + 分隔符 + 时间戳
|
|
333
|
+
const data = `${token}|+|${time}`;
|
|
334
|
+
// 使用 AES-CBC 模式加密
|
|
335
|
+
return CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(this.config.key), {
|
|
336
|
+
iv: CryptoJS.enc.Utf8.parse(this.config.iv),
|
|
337
|
+
mode: CryptoJS.mode.CBC,
|
|
338
|
+
}).toString();
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 解密 Token
|
|
342
|
+
*
|
|
343
|
+
* 将加密后的 Token 解密,提取原始 Token 和时间戳
|
|
344
|
+
*
|
|
345
|
+
* @param encryptedToken - 加密后的 Token 字符串
|
|
346
|
+
* @returns 包含原始 Token 和时间戳的对象
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```ts
|
|
350
|
+
* const { token, timestamp } = encryption.decryptToken(encryptedToken);
|
|
351
|
+
* console.log(token); // 'my-token'
|
|
352
|
+
* console.log(timestamp); // 1703577600000
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
decryptToken(encryptedToken) {
|
|
356
|
+
// 使用 AES-CBC 模式解密
|
|
357
|
+
const decrypted = CryptoJS.AES.decrypt(encryptedToken, CryptoJS.enc.Utf8.parse(this.config.key), {
|
|
358
|
+
iv: CryptoJS.enc.Utf8.parse(this.config.iv),
|
|
359
|
+
mode: CryptoJS.mode.CBC,
|
|
360
|
+
}).toString(CryptoJS.enc.Utf8);
|
|
361
|
+
// 分割原始数据,提取 Token 和时间戳
|
|
362
|
+
const parts = decrypted.split('|+|');
|
|
363
|
+
return {
|
|
364
|
+
token: parts[0],
|
|
365
|
+
timestamp: parseInt(parts[1], 10),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 获取调整后的时间戳
|
|
370
|
+
*
|
|
371
|
+
* 优先使用时间同步服务获取校正后的时间
|
|
372
|
+
* 如果没有时间同步服务,则使用当前客户端时间
|
|
373
|
+
*
|
|
374
|
+
* @returns 时间戳(毫秒)
|
|
375
|
+
*/
|
|
376
|
+
getAdjustedTime() {
|
|
377
|
+
if (this.timeSyncService) {
|
|
378
|
+
return this.timeSyncService.getAdjustedTime();
|
|
379
|
+
}
|
|
380
|
+
return Date.now();
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 更新配置
|
|
384
|
+
*
|
|
385
|
+
* 动态更新加密服务的配置
|
|
386
|
+
*
|
|
387
|
+
* @param config - 要更新的配置项
|
|
388
|
+
*/
|
|
389
|
+
updateConfig(config) {
|
|
390
|
+
this.config = { ...this.config, ...config };
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 设置时间同步服务
|
|
394
|
+
*
|
|
395
|
+
* 用于后期绑定时间同步服务实例
|
|
396
|
+
*
|
|
397
|
+
* @param timeSyncService - 时间同步服务实例
|
|
398
|
+
*/
|
|
399
|
+
setTimeSyncService(timeSyncService) {
|
|
400
|
+
this.timeSyncService = timeSyncService;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 请求拦截器
|
|
406
|
+
*
|
|
407
|
+
* 在请求发出前对请求进行处理,包括:
|
|
408
|
+
* - 时间同步
|
|
409
|
+
* - 设置默认请求头
|
|
410
|
+
* - 添加用户 ID
|
|
411
|
+
* - Token 加密和添加
|
|
412
|
+
* - 调用自定义拦截器
|
|
413
|
+
*
|
|
414
|
+
* @module interceptors/request
|
|
415
|
+
*/
|
|
416
|
+
/**
|
|
417
|
+
* 设置请求拦截器
|
|
418
|
+
*
|
|
419
|
+
* 为 Axios 实例添加请求拦截器,处理请求前的通用逻辑
|
|
420
|
+
*
|
|
421
|
+
* 拦截器执行顺序:
|
|
422
|
+
* 1. 确保时间同步(如果配置了 TimeSyncService)
|
|
423
|
+
* 2. 设置默认 Content-Type
|
|
424
|
+
* 3. 添加用户 ID 到请求头
|
|
425
|
+
* 4. 加密并添加 Token 到请求头
|
|
426
|
+
* 5. 调用自定义请求拦截器
|
|
427
|
+
*
|
|
428
|
+
* @param instance - Axios 实例
|
|
429
|
+
* @param options - 请求配置选项
|
|
430
|
+
* @param timeSyncService - 时间同步服务实例(可选)
|
|
431
|
+
* @param encryptionService - 加密服务实例(可选)
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```ts
|
|
435
|
+
* const instance = axios.create({ baseURL: 'https://api.example.com' });
|
|
436
|
+
*
|
|
437
|
+
* setRequestInterceptor(
|
|
438
|
+
* instance,
|
|
439
|
+
* {
|
|
440
|
+
* baseURL: 'https://api.example.com',
|
|
441
|
+
* getToken: () => localStorage.getItem('token'),
|
|
442
|
+
* getUserId: () => localStorage.getItem('userId'),
|
|
443
|
+
* },
|
|
444
|
+
* timeSyncService,
|
|
445
|
+
* encryptionService,
|
|
446
|
+
* );
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
function setRequestInterceptor(instance, options, timeSyncService, encryptionService) {
|
|
450
|
+
// 获取 header 名称,使用默认值
|
|
451
|
+
const tokenHeaderName = options.tokenHeaderName || 'X-BeLink-Token';
|
|
452
|
+
const userIdHeaderName = options.userIdHeaderName || 'X-BeLink-UserId';
|
|
453
|
+
instance.interceptors.request.use(async (config) => {
|
|
454
|
+
// 步骤 1: 确保时间同步
|
|
455
|
+
// 在请求发出前检查并同步服务器时间,确保后续 Token 加密使用正确的时间戳
|
|
456
|
+
if (timeSyncService) {
|
|
457
|
+
await timeSyncService.ensureSync();
|
|
458
|
+
}
|
|
459
|
+
// 步骤 2: 设置默认 Content-Type
|
|
460
|
+
// 如果请求未指定 Content-Type,默认使用 JSON 格式
|
|
461
|
+
if (!config.headers['Content-Type']) {
|
|
462
|
+
config.headers['Content-Type'] = 'application/json';
|
|
463
|
+
}
|
|
464
|
+
// 步骤 3: 添加用户 ID
|
|
465
|
+
// 调用用户配置的 getUserId 函数获取用户 ID,添加到请求头
|
|
466
|
+
if (options.getUserId) {
|
|
467
|
+
const userId = options.getUserId();
|
|
468
|
+
if (userId) {
|
|
469
|
+
config.headers[userIdHeaderName] = userId;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// 步骤 4: 添加 Token
|
|
473
|
+
// 根据是否配置了加密服务,决定是否对 Token 进行加密
|
|
474
|
+
if (options.getToken && encryptionService) {
|
|
475
|
+
// 有加密服务:获取 Token 并加密后添加到请求头
|
|
476
|
+
const token = options.getToken();
|
|
477
|
+
if (token) {
|
|
478
|
+
config.headers[tokenHeaderName] = encryptionService.encryptToken(token);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else if (options.getToken) {
|
|
482
|
+
// 无加密服务:直接使用原始 Token
|
|
483
|
+
const token = options.getToken();
|
|
484
|
+
if (token) {
|
|
485
|
+
config.headers[tokenHeaderName] = token;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// 步骤 5: 调用自定义请求拦截器
|
|
489
|
+
// 允许用户进一步自定义请求配置
|
|
490
|
+
if (options.onRequest) {
|
|
491
|
+
return options.onRequest(config);
|
|
492
|
+
}
|
|
493
|
+
return config;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 响应拦截器
|
|
499
|
+
*
|
|
500
|
+
* 在收到响应后对响应进行处理,包括:
|
|
501
|
+
* - 提取响应数据
|
|
502
|
+
* - 调用自定义响应拦截器
|
|
503
|
+
* - 统一错误处理
|
|
504
|
+
*
|
|
505
|
+
* @module interceptors/response
|
|
506
|
+
*/
|
|
507
|
+
/**
|
|
508
|
+
* 设置响应拦截器
|
|
509
|
+
*
|
|
510
|
+
* 为 Axios 实例添加响应拦截器,处理响应后的通用逻辑
|
|
511
|
+
*
|
|
512
|
+
* 成功响应处理:
|
|
513
|
+
* 1. 提取响应数据(response.data)
|
|
514
|
+
* 2. 调用自定义响应拦截器(如果配置)
|
|
515
|
+
* 3. 返回处理后的数据
|
|
516
|
+
*
|
|
517
|
+
* 错误响应处理:
|
|
518
|
+
* 1. 调用自定义错误处理(如果配置)
|
|
519
|
+
* 2. 检查业务错误(success === false)
|
|
520
|
+
* 3. 抛出错误
|
|
521
|
+
*
|
|
522
|
+
* @param instance - Axios 实例
|
|
523
|
+
* @param options - 请求配置选项
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* const instance = axios.create({ baseURL: 'https://api.example.com' });
|
|
528
|
+
*
|
|
529
|
+
* setResponseInterceptor(instance, {
|
|
530
|
+
* baseURL: 'https://api.example.com',
|
|
531
|
+
* onResponse: (data) => {
|
|
532
|
+
* console.log('响应数据:', data);
|
|
533
|
+
* return data;
|
|
534
|
+
* },
|
|
535
|
+
* onError: (error) => {
|
|
536
|
+
* if (error.response?.status === 401) {
|
|
537
|
+
* // 处理未授权错误
|
|
538
|
+
* window.location.href = '/login';
|
|
539
|
+
* }
|
|
540
|
+
* throw error;
|
|
541
|
+
* },
|
|
542
|
+
* });
|
|
543
|
+
* ```
|
|
544
|
+
*/
|
|
545
|
+
function setResponseInterceptor(instance, options) {
|
|
546
|
+
instance.interceptors.response.use(
|
|
547
|
+
// 成功响应处理
|
|
548
|
+
(response) => {
|
|
549
|
+
// 提取响应数据(Axios 的响应数据在 response.data 中)
|
|
550
|
+
const data = response.data;
|
|
551
|
+
// 调用自定义响应拦截器
|
|
552
|
+
// 允许用户对响应数据进行进一步处理
|
|
553
|
+
if (options.onResponse) {
|
|
554
|
+
return options.onResponse(data);
|
|
555
|
+
}
|
|
556
|
+
return data;
|
|
557
|
+
},
|
|
558
|
+
// 错误响应处理
|
|
559
|
+
(error) => {
|
|
560
|
+
// 优先调用自定义错误处理
|
|
561
|
+
// 如果用户配置了 onError,由用户自行处理错误
|
|
562
|
+
if (options.onError) {
|
|
563
|
+
return options.onError(error);
|
|
564
|
+
}
|
|
565
|
+
// 获取响应数据(如果有)
|
|
566
|
+
const responseData = error?.response?.data;
|
|
567
|
+
// 检查业务错误
|
|
568
|
+
// 业务错误:HTTP 状态码可能是 200,但响应体中 success === false
|
|
569
|
+
if (responseData && typeof responseData === 'object' && !responseData.success) {
|
|
570
|
+
// 创建业务错误对象
|
|
571
|
+
const bizError = new Error(responseData.message || '接口请求失败');
|
|
572
|
+
// 附加原始响应信息,方便调试
|
|
573
|
+
bizError.response = error.response;
|
|
574
|
+
bizError.data = responseData;
|
|
575
|
+
throw bizError;
|
|
576
|
+
}
|
|
577
|
+
// 其他错误(网络错误、超时等)直接抛出
|
|
578
|
+
throw error;
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* BeLinkHttp 请求客户端
|
|
584
|
+
*
|
|
585
|
+
* 单例模式的 HTTP 请求客户端
|
|
586
|
+
* 支持时间同步和 Token 加密
|
|
587
|
+
*
|
|
588
|
+
* @module BeLinkHttp
|
|
589
|
+
*
|
|
590
|
+
* @example
|
|
591
|
+
* ```ts
|
|
592
|
+
* import { beLinkHttp } from '@be-link/http';
|
|
593
|
+
*
|
|
594
|
+
* // 初始化
|
|
595
|
+
* beLinkHttp.init({
|
|
596
|
+
* baseURL: 'https://api.example.com',
|
|
597
|
+
* timeSync: { syncUrl: 'https://api.example.com/api/time' },
|
|
598
|
+
* encryption: { key: '...', iv: '...' },
|
|
599
|
+
* getToken: () => localStorage.getItem('token'),
|
|
600
|
+
* });
|
|
601
|
+
*
|
|
602
|
+
* // 发起请求
|
|
603
|
+
* const data = await beLinkHttp.get('/api/users');
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
/**
|
|
607
|
+
* BeLinkHttp 单例类
|
|
608
|
+
*
|
|
609
|
+
* 提供统一的 HTTP 请求接口,支持:
|
|
610
|
+
* - 时间同步
|
|
611
|
+
* - Token 加密
|
|
612
|
+
* - 请求/响应拦截
|
|
613
|
+
*/
|
|
614
|
+
class BeLinkHttp {
|
|
615
|
+
constructor() {
|
|
616
|
+
/** Axios 实例 */
|
|
617
|
+
this.instance = null;
|
|
618
|
+
/** 时间同步服务 */
|
|
619
|
+
this.timeSyncService = null;
|
|
620
|
+
/** 加密服务 */
|
|
621
|
+
this.encryptionService = null;
|
|
622
|
+
/** 是否已初始化 */
|
|
623
|
+
this.initialized = false;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* 初始化请求客户端
|
|
627
|
+
*
|
|
628
|
+
* 必须在使用其他方法之前调用此方法进行初始化
|
|
629
|
+
*
|
|
630
|
+
* @param options - 请求配置选项
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```ts
|
|
634
|
+
* beLinkHttp.init({
|
|
635
|
+
* baseURL: 'https://api.example.com',
|
|
636
|
+
* timeout: 30000,
|
|
637
|
+
* timeSync: {
|
|
638
|
+
* syncUrl: 'https://api.example.com/api/time',
|
|
639
|
+
* },
|
|
640
|
+
* encryption: {
|
|
641
|
+
* key: 'your-aes-key-16ch',
|
|
642
|
+
* iv: 'your-aes-iv-16ch',
|
|
643
|
+
* },
|
|
644
|
+
* getToken: () => localStorage.getItem('token'),
|
|
645
|
+
* getUserId: () => localStorage.getItem('userId'),
|
|
646
|
+
* });
|
|
647
|
+
* ```
|
|
648
|
+
*/
|
|
649
|
+
init(options) {
|
|
650
|
+
const { baseURL, timeout = 30000, headers = {}, timeSync, encryption } = options;
|
|
651
|
+
// 创建 Axios 实例
|
|
652
|
+
this.instance = axios.create({
|
|
653
|
+
baseURL,
|
|
654
|
+
timeout,
|
|
655
|
+
headers: {
|
|
656
|
+
'Content-Type': 'application/json',
|
|
657
|
+
...headers,
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
// 创建时间同步服务
|
|
661
|
+
if (timeSync?.syncUrl) {
|
|
662
|
+
this.timeSyncService = new TimeSyncService(timeSync);
|
|
663
|
+
}
|
|
664
|
+
// 创建加密服务
|
|
665
|
+
if (encryption?.key && encryption?.iv) {
|
|
666
|
+
this.encryptionService = new EncryptionService(encryption, this.timeSyncService || undefined);
|
|
667
|
+
}
|
|
668
|
+
// 设置拦截器
|
|
669
|
+
setRequestInterceptor(this.instance, options, this.timeSyncService || undefined, this.encryptionService || undefined);
|
|
670
|
+
setResponseInterceptor(this.instance, options);
|
|
671
|
+
this.initialized = true;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* 检查是否已初始化
|
|
675
|
+
*/
|
|
676
|
+
checkInitialized() {
|
|
677
|
+
if (!this.initialized || !this.instance) {
|
|
678
|
+
throw new Error('[BeLinkHttp] 请先调用 init() 方法进行初始化');
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* GET 请求
|
|
683
|
+
*
|
|
684
|
+
* @param url - 请求 URL
|
|
685
|
+
* @param config - 可选的 Axios 请求配置
|
|
686
|
+
* @returns Promise 响应数据
|
|
687
|
+
*
|
|
688
|
+
* @example
|
|
689
|
+
* ```ts
|
|
690
|
+
* const users = await beLinkHttp.get('/api/users');
|
|
691
|
+
* const user = await beLinkHttp.get('/api/users/1', { params: { include: 'profile' } });
|
|
692
|
+
* ```
|
|
693
|
+
*/
|
|
694
|
+
async get(url, config) {
|
|
695
|
+
this.checkInitialized();
|
|
696
|
+
return this.instance.get(url, config);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* POST 请求
|
|
700
|
+
*
|
|
701
|
+
* @param url - 请求 URL
|
|
702
|
+
* @param data - 请求体数据
|
|
703
|
+
* @param config - 可选的 Axios 请求配置
|
|
704
|
+
* @returns Promise 响应数据
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* ```ts
|
|
708
|
+
* const result = await beLinkHttp.post('/api/users', { name: '张三', age: 25 });
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
711
|
+
async post(url, data, config) {
|
|
712
|
+
this.checkInitialized();
|
|
713
|
+
return this.instance.post(url, data, config);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* PUT 请求
|
|
717
|
+
*
|
|
718
|
+
* @param url - 请求 URL
|
|
719
|
+
* @param data - 请求体数据
|
|
720
|
+
* @param config - 可选的 Axios 请求配置
|
|
721
|
+
* @returns Promise 响应数据
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```ts
|
|
725
|
+
* const result = await beLinkHttp.put('/api/users/1', { name: '李四' });
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
728
|
+
async put(url, data, config) {
|
|
729
|
+
this.checkInitialized();
|
|
730
|
+
return this.instance.put(url, data, config);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* PATCH 请求
|
|
734
|
+
*
|
|
735
|
+
* @param url - 请求 URL
|
|
736
|
+
* @param data - 请求体数据
|
|
737
|
+
* @param config - 可选的 Axios 请求配置
|
|
738
|
+
* @returns Promise 响应数据
|
|
739
|
+
*
|
|
740
|
+
* @example
|
|
741
|
+
* ```ts
|
|
742
|
+
* const result = await beLinkHttp.patch('/api/users/1', { age: 26 });
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
async patch(url, data, config) {
|
|
746
|
+
this.checkInitialized();
|
|
747
|
+
return this.instance.patch(url, data, config);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* DELETE 请求
|
|
751
|
+
*
|
|
752
|
+
* @param url - 请求 URL
|
|
753
|
+
* @param config - 可选的 Axios 请求配置
|
|
754
|
+
* @returns Promise 响应数据
|
|
755
|
+
*
|
|
756
|
+
* @example
|
|
757
|
+
* ```ts
|
|
758
|
+
* await beLinkHttp.delete('/api/users/1');
|
|
759
|
+
* ```
|
|
760
|
+
*/
|
|
761
|
+
async delete(url, config) {
|
|
762
|
+
this.checkInitialized();
|
|
763
|
+
return this.instance.delete(url, config);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* 通用请求方法
|
|
767
|
+
*
|
|
768
|
+
* @param config - Axios 请求配置
|
|
769
|
+
* @returns Promise 响应数据
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```ts
|
|
773
|
+
* const result = await beLinkHttp.request({
|
|
774
|
+
* method: 'post',
|
|
775
|
+
* url: '/api/users',
|
|
776
|
+
* data: { name: '张三' },
|
|
777
|
+
* headers: { 'X-Custom': 'value' },
|
|
778
|
+
* });
|
|
779
|
+
* ```
|
|
780
|
+
*/
|
|
781
|
+
async request(config) {
|
|
782
|
+
this.checkInitialized();
|
|
783
|
+
return this.instance.request(config);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* 获取时间同步服务实例
|
|
787
|
+
*
|
|
788
|
+
* @returns 时间同步服务实例,未初始化时返回 null
|
|
789
|
+
*/
|
|
790
|
+
getTimeSyncService() {
|
|
791
|
+
return this.timeSyncService;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* 获取加密服务实例
|
|
795
|
+
*
|
|
796
|
+
* @returns 加密服务实例,未初始化时返回 null
|
|
797
|
+
*/
|
|
798
|
+
getEncryptionService() {
|
|
799
|
+
return this.encryptionService;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* 获取 Axios 实例
|
|
803
|
+
*
|
|
804
|
+
* @returns Axios 实例,未初始化时返回 null
|
|
805
|
+
*/
|
|
806
|
+
getAxiosInstance() {
|
|
807
|
+
return this.instance;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* 重置客户端
|
|
811
|
+
*
|
|
812
|
+
* 清除所有配置和实例,需要重新调用 init() 初始化
|
|
813
|
+
*/
|
|
814
|
+
reset() {
|
|
815
|
+
this.instance = null;
|
|
816
|
+
this.timeSyncService = null;
|
|
817
|
+
this.encryptionService = null;
|
|
818
|
+
this.initialized = false;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* BeLinkHttp 单例实例
|
|
823
|
+
*
|
|
824
|
+
* @example
|
|
825
|
+
* ```ts
|
|
826
|
+
* import { beLinkHttp } from '@be-link/http';
|
|
827
|
+
*
|
|
828
|
+
* // 初始化
|
|
829
|
+
* beLinkHttp.init({
|
|
830
|
+
* baseURL: 'https://api.example.com',
|
|
831
|
+
* });
|
|
832
|
+
*
|
|
833
|
+
* // 使用
|
|
834
|
+
* const data = await beLinkHttp.get('/api/users');
|
|
835
|
+
* ```
|
|
836
|
+
*/
|
|
837
|
+
const beLinkHttp = new BeLinkHttp();
|
|
838
|
+
|
|
839
|
+
exports.BeLinkHttp = BeLinkHttp;
|
|
840
|
+
exports.EncryptionService = EncryptionService;
|
|
841
|
+
exports.TimeSyncService = TimeSyncService;
|
|
842
|
+
exports.beLinkHttp = beLinkHttp;
|
|
843
|
+
exports.default = beLinkHttp;
|
|
844
|
+
exports.setRequestInterceptor = setRequestInterceptor;
|
|
845
|
+
exports.setResponseInterceptor = setResponseInterceptor;
|