zmdms-utils 0.0.96 → 0.0.98

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/dist/es/file.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getPreToken } from './auth.js';
2
2
  import { dangerouslyXss, specialString, getFileExtension, isImageExtension } from './common.js';
3
3
  import { BASIC_KEY, TOKEN_KEY, FILE_SERVICE, FILE_PRVIEW_SERVICE_URL } from './constants.js';
4
- import { Crypto } from './crypto.js';
4
+ import { getOssCrypto } from './securityKey.js';
5
5
 
6
6
  /**
7
7
  * 附件相关接口
@@ -418,7 +418,10 @@ function getBasicInfo(baseURL, token) {
418
418
  */
419
419
  function encodeFileId(fileId, encodeNum) {
420
420
  if (encodeNum === void 0) { encodeNum = 1; }
421
- var crypto = new Crypto(window.__CRYPTO_AES_KEY__, window.__CRYPTO_DES_KEY__);
421
+ var crypto = getOssCrypto();
422
+ if (!crypto) {
423
+ throw new Error("缺少附件加密密钥,请先调用initSecurityKeys初始化或设置window.__CRYPTO_AES_KEY__");
424
+ }
422
425
  if (encodeNum === 0) {
423
426
  return crypto.encrypt(fileId);
424
427
  }
@@ -23,6 +23,11 @@ declare class AxiosRequest {
23
23
  private responseError;
24
24
  private logoutHandle;
25
25
  request(options: IOptions): Promise<AxiosResponse<any, any>>;
26
+ /**
27
+ * 推断密钥接口的请求前缀地址
28
+ * 绝对地址的请求取其origin 其他情况取实例的baseURL(同源场景为空串)
29
+ */
30
+ private getSecurityKeyBaseURL;
26
31
  }
27
32
  interface IOptions extends AxiosRequestConfig {
28
33
  isFormData?: boolean;
@@ -75,6 +80,12 @@ interface IOtherOptions {
75
80
  baseURL?: string;
76
81
  /** 加密实例方法 */
77
82
  crypto?: Crypto;
83
+ /**
84
+ * 开启自动密钥模式
85
+ * 开启后无需传crypto 加密请求会自动等待密钥就绪后加密
86
+ * 带token的请求会顺带预热密钥
87
+ */
88
+ securityKey?: boolean;
78
89
  /** 超时是否需要弹框 */
79
90
  isTimeoutConfirm?: boolean;
80
91
  /** 超时弹框提示内容 */
@@ -1,6 +1,7 @@
1
- import { __rest, __assign } from './_virtual/_tslib.js';
1
+ import { __awaiter, __rest, __assign, __generator } from './_virtual/_tslib.js';
2
2
  import axios from 'axios';
3
3
  import { removeToken, getPreToken } from './auth.js';
4
+ import { getApiCrypto, initSecurityKeys } from './securityKey.js';
4
5
  import { TOKEN_KEY } from './constants.js';
5
6
 
6
7
  var defaultOptions = {
@@ -236,67 +237,106 @@ var AxiosRequest = /** @class */ (function () {
236
237
  };
237
238
  // 实例方法
238
239
  AxiosRequest.prototype.request = function (options) {
239
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
240
- var isFormData = options.isFormData, _k = options.isAuth, isAuth = _k === void 0 ? true : _k, resetOptions = __rest(options, ["isFormData", "isAuth"]);
241
- var newOptions = resetOptions;
242
- // 自定义请求头
243
- if (!newOptions.headers) {
244
- newOptions.headers = {};
245
- }
246
- if (isAuth) {
247
- var token = getPreToken();
248
- if (token) {
249
- newOptions.headers[this.tokenKey] = token;
250
- }
251
- }
252
- // 遍历params,将undefined值 转为null值
253
- if (newOptions.data) {
254
- newOptions.data = transformData(newOptions.data);
255
- }
256
- if (newOptions.params) {
257
- newOptions.params = transformData(newOptions.params);
258
- }
259
- // 处理特殊请求
260
- var specialMethod = ["POST", "PUT", "PATCH"];
261
- var method = ((_a = newOptions.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || "";
262
- if (specialMethod.includes(method)) {
263
- if (isFormData) {
264
- var formData = new FormData();
265
- var dataObj = newOptions.data || {};
266
- var dataKey = Object.keys(dataObj);
267
- var dataVal = Object.values(dataObj);
268
- for (var i = 0; i < dataKey.length; i++) {
269
- if (dataVal[i]) {
270
- formData.append(dataKey[i], dataVal[i]);
271
- }
240
+ var _a, _b, _c, _d, _e;
241
+ return __awaiter(this, void 0, void 0, function () {
242
+ var isFormData, _f, isAuth, resetOptions, newOptions, token, specialMethod, method, formData, dataObj, dataKey, dataVal, i, crypto;
243
+ return __generator(this, function (_g) {
244
+ switch (_g.label) {
245
+ case 0:
246
+ isFormData = options.isFormData, _f = options.isAuth, isAuth = _f === void 0 ? true : _f, resetOptions = __rest(options, ["isFormData", "isAuth"]);
247
+ newOptions = resetOptions;
248
+ // 自定义请求头
249
+ if (!newOptions.headers) {
250
+ newOptions.headers = {};
251
+ }
252
+ if (isAuth) {
253
+ token = getPreToken();
254
+ if (token) {
255
+ newOptions.headers[this.tokenKey] = token;
256
+ }
257
+ // 自动密钥模式:带token的请求顺带预热密钥 不阻塞当前请求
258
+ // 预热失败静默处理 真正需要密钥的请求会再走到等待逻辑并报错
259
+ if (this.otherOptions.securityKey) {
260
+ initSecurityKeys({
261
+ baseURL: this.getSecurityKeyBaseURL(options),
262
+ }).catch(function () { });
263
+ }
264
+ }
265
+ if (!(newOptions.isCrypto && !this.crypto && this.otherOptions.securityKey)) return [3 /*break*/, 2];
266
+ return [4 /*yield*/, initSecurityKeys({
267
+ baseURL: this.getSecurityKeyBaseURL(options),
268
+ })];
269
+ case 1:
270
+ _g.sent();
271
+ _g.label = 2;
272
+ case 2:
273
+ // 遍历params,将undefined值 转为null值
274
+ if (newOptions.data) {
275
+ newOptions.data = transformData(newOptions.data);
276
+ }
277
+ if (newOptions.params) {
278
+ newOptions.params = transformData(newOptions.params);
279
+ }
280
+ specialMethod = ["POST", "PUT", "PATCH"];
281
+ method = ((_a = newOptions.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || "";
282
+ if (specialMethod.includes(method)) {
283
+ if (isFormData) {
284
+ formData = new FormData();
285
+ dataObj = newOptions.data || {};
286
+ dataKey = Object.keys(dataObj);
287
+ dataVal = Object.values(dataObj);
288
+ for (i = 0; i < dataKey.length; i++) {
289
+ if (dataVal[i]) {
290
+ formData.append(dataKey[i], dataVal[i]);
291
+ }
292
+ }
293
+ newOptions.headers["Content-Type"] = "multipart/form-data";
294
+ newOptions.data = formData;
295
+ }
296
+ else {
297
+ newOptions.headers = __assign({ "Content-Type": "application/json" }, newOptions.headers);
298
+ }
299
+ }
300
+ crypto = this.crypto ||
301
+ (this.otherOptions.securityKey ? getApiCrypto() : undefined);
302
+ // 将参数进行aes加密
303
+ if (newOptions.isCrypto === true || newOptions.isCrypto === "aes") {
304
+ if (newOptions.data) {
305
+ newOptions.data = (_b = crypto === null || crypto === void 0 ? void 0 : crypto.encrypt) === null || _b === void 0 ? void 0 : _b.call(crypto, JSON.stringify(newOptions.data));
306
+ }
307
+ if (newOptions.params) {
308
+ newOptions.params = (_c = crypto === null || crypto === void 0 ? void 0 : crypto.encrypt) === null || _c === void 0 ? void 0 : _c.call(crypto, JSON.stringify(newOptions.params));
309
+ }
310
+ }
311
+ // 将参数进行des加密
312
+ if (newOptions.isCrypto === "des") {
313
+ if (newOptions.data) {
314
+ newOptions.data = (_d = crypto === null || crypto === void 0 ? void 0 : crypto.encrypt_des) === null || _d === void 0 ? void 0 : _d.call(crypto, JSON.stringify(newOptions.data));
315
+ }
316
+ if (newOptions.params) {
317
+ newOptions.params = (_e = crypto === null || crypto === void 0 ? void 0 : crypto.encrypt_des) === null || _e === void 0 ? void 0 : _e.call(crypto, JSON.stringify(newOptions.params));
318
+ }
319
+ }
320
+ return [2 /*return*/, this.instanceAxios(newOptions)];
272
321
  }
273
- newOptions.headers["Content-Type"] = "multipart/form-data";
274
- newOptions.data = formData;
275
- }
276
- else {
277
- newOptions.headers = __assign({ "Content-Type": "application/json" }, newOptions.headers);
278
- }
279
- }
280
- // 数据加密处理 TODO:
281
- // 将参数进行aes加密
282
- if (newOptions.isCrypto === true || newOptions.isCrypto === "aes") {
283
- if (newOptions.data) {
284
- newOptions.data = (_c = (_b = this.crypto) === null || _b === void 0 ? void 0 : _b.encrypt) === null || _c === void 0 ? void 0 : _c.call(_b, JSON.stringify(newOptions.data));
285
- }
286
- if (newOptions.params) {
287
- newOptions.params = (_e = (_d = this.crypto) === null || _d === void 0 ? void 0 : _d.encrypt) === null || _e === void 0 ? void 0 : _e.call(_d, JSON.stringify(newOptions.params));
288
- }
289
- }
290
- // 将参数进行des加密
291
- if (newOptions.isCrypto === "des") {
292
- if (newOptions.data) {
293
- newOptions.data = (_g = (_f = this.crypto) === null || _f === void 0 ? void 0 : _f.encrypt_des) === null || _g === void 0 ? void 0 : _g.call(_f, JSON.stringify(newOptions.data));
322
+ });
323
+ });
324
+ };
325
+ /**
326
+ * 推断密钥接口的请求前缀地址
327
+ * 绝对地址的请求取其origin 其他情况取实例的baseURL(同源场景为空串)
328
+ */
329
+ AxiosRequest.prototype.getSecurityKeyBaseURL = function (options) {
330
+ var url = options.url || "";
331
+ if (/^https?:\/\//i.test(url)) {
332
+ try {
333
+ return new URL(url).origin;
294
334
  }
295
- if (newOptions.params) {
296
- newOptions.params = (_j = (_h = this.crypto) === null || _h === void 0 ? void 0 : _h.encrypt_des) === null || _j === void 0 ? void 0 : _j.call(_h, JSON.stringify(newOptions.params));
335
+ catch (err) {
336
+ console.log(err);
297
337
  }
298
338
  }
299
- return this.instanceAxios(newOptions);
339
+ return this.otherOptions.baseURL || "";
300
340
  };
301
341
  return AxiosRequest;
302
342
  }());
@@ -0,0 +1,49 @@
1
+ import { Crypto } from './crypto.js';
2
+
3
+ /**
4
+ * 密钥集合
5
+ */
6
+ interface ISecurityKeys {
7
+ /** 接口请求参数加密用 AES 密钥 */
8
+ apiAesKey: string;
9
+ /** 接口请求参数加密用 DES 密钥 */
10
+ apiDesKey: string;
11
+ /** 附件(OSS)相关加密用 AES 密钥 */
12
+ ossAesKey: string;
13
+ /** 附件(OSS)相关加密用 DES 密钥 */
14
+ ossDesKey: string;
15
+ }
16
+ interface ISecurityKeysOptions {
17
+ /** 接口请求的前缀地址 必传 */
18
+ baseURL: string;
19
+ }
20
+ /**
21
+ * 初始化安全密钥
22
+ * 应用启动时(render之前)调用一次 并发调用会共享同一个请求
23
+ * @param options
24
+ * @param options.baseURL {string} 接口请求的前缀地址 必传
25
+ * @returns 密钥集合
26
+ */
27
+ declare function initSecurityKeys(options: ISecurityKeysOptions): Promise<ISecurityKeys>;
28
+ /**
29
+ * 强制刷新安全密钥
30
+ * 后端轮换密钥后可调用 会忽略已有缓存重新请求
31
+ * @param options
32
+ * @param options.baseURL {string} 接口请求的前缀地址 必传
33
+ * @returns 密钥集合
34
+ */
35
+ declare function refreshSecurityKeys(options: ISecurityKeysOptions): Promise<ISecurityKeys>;
36
+ /**
37
+ * 获取接口请求参数加密的Crypto实例
38
+ * 未初始化时回退读取window上的旧密钥(兼容未接入initSecurityKeys的应用)
39
+ * @returns Crypto实例 未获取到密钥时返回undefined
40
+ */
41
+ declare function getApiCrypto(): Crypto | undefined;
42
+ /**
43
+ * 获取附件(OSS)相关加密的Crypto实例
44
+ * 未初始化时回退读取window上的旧密钥(兼容未接入initSecurityKeys的应用)
45
+ * @returns Crypto实例 未获取到密钥时返回undefined
46
+ */
47
+ declare function getOssCrypto(): Crypto | undefined;
48
+
49
+ export { ISecurityKeys, getApiCrypto, getOssCrypto, initSecurityKeys, refreshSecurityKeys };
@@ -0,0 +1,121 @@
1
+ import { __awaiter, __generator } from './_virtual/_tslib.js';
2
+ import axios from 'axios';
3
+ import { getPreToken } from './auth.js';
4
+ import { TOKEN_KEY, BASIC_KEY } from './constants.js';
5
+ import { Crypto } from './crypto.js';
6
+
7
+ // 密钥内存缓存 不挂载到window 避免产生新的明文全局变量
8
+ var securityKeys = null;
9
+ // 进行中的密钥请求 并发调用initSecurityKeys/refreshSecurityKeys时共享同一个请求
10
+ var securityKeysPromise = null;
11
+ /**
12
+ * 初始化安全密钥
13
+ * 应用启动时(render之前)调用一次 并发调用会共享同一个请求
14
+ * @param options
15
+ * @param options.baseURL {string} 接口请求的前缀地址 必传
16
+ * @returns 密钥集合
17
+ */
18
+ function initSecurityKeys(options) {
19
+ if (securityKeys) {
20
+ return Promise.resolve(securityKeys);
21
+ }
22
+ if (securityKeysPromise) {
23
+ return securityKeysPromise;
24
+ }
25
+ return requestSecurityKeys(options);
26
+ }
27
+ /**
28
+ * 强制刷新安全密钥
29
+ * 后端轮换密钥后可调用 会忽略已有缓存重新请求
30
+ * @param options
31
+ * @param options.baseURL {string} 接口请求的前缀地址 必传
32
+ * @returns 密钥集合
33
+ */
34
+ function refreshSecurityKeys(options) {
35
+ return requestSecurityKeys(options);
36
+ }
37
+ /**
38
+ * 获取接口请求参数加密的Crypto实例
39
+ * 未初始化时回退读取window上的旧密钥(兼容未接入initSecurityKeys的应用)
40
+ * @returns Crypto实例 未获取到密钥时返回undefined
41
+ */
42
+ function getApiCrypto() {
43
+ if (securityKeys === null || securityKeys === void 0 ? void 0 : securityKeys.apiAesKey) {
44
+ return new Crypto(securityKeys.apiAesKey, securityKeys.apiDesKey);
45
+ }
46
+ return createCryptoFromWindow();
47
+ }
48
+ /**
49
+ * 获取附件(OSS)相关加密的Crypto实例
50
+ * 未初始化时回退读取window上的旧密钥(兼容未接入initSecurityKeys的应用)
51
+ * @returns Crypto实例 未获取到密钥时返回undefined
52
+ */
53
+ function getOssCrypto() {
54
+ if (securityKeys === null || securityKeys === void 0 ? void 0 : securityKeys.ossAesKey) {
55
+ return new Crypto(securityKeys.ossAesKey, securityKeys.ossDesKey);
56
+ }
57
+ return createCryptoFromWindow();
58
+ }
59
+ /**
60
+ * 请求密钥并缓存
61
+ */
62
+ function requestSecurityKeys(options) {
63
+ securityKeysPromise = fetchSecurityKeys(options).then(function (keys) {
64
+ securityKeys = keys;
65
+ securityKeysPromise = null;
66
+ return keys;
67
+ }, function (err) {
68
+ securityKeysPromise = null;
69
+ throw err;
70
+ });
71
+ return securityKeysPromise;
72
+ }
73
+ /**
74
+ * 请求密钥接口
75
+ * 使用裸axios请求(不走AxiosRequest) 避免请求本身依赖密钥或触发统一的异常处理
76
+ */
77
+ function fetchSecurityKeys(options) {
78
+ var _a, _b;
79
+ return __awaiter(this, void 0, void 0, function () {
80
+ var apiBaseURL, headers, token, response, _c, code, data, msg;
81
+ return __generator(this, function (_d) {
82
+ switch (_d.label) {
83
+ case 0:
84
+ apiBaseURL = ((_b = (_a = options.baseURL) === null || _a === void 0 ? void 0 : _a.endsWith) === null || _b === void 0 ? void 0 : _b.call(_a, "/"))
85
+ ? options.baseURL.slice(0, options.baseURL.length - 1)
86
+ : options.baseURL;
87
+ headers = {};
88
+ token = getPreToken();
89
+ if (token) {
90
+ headers[TOKEN_KEY] = token;
91
+ }
92
+ return [4 /*yield*/, axios.get("".concat(apiBaseURL, "/api/").concat(BASIC_KEY, "-resource/resource/get-security-key"), { headers: headers, timeout: 60000 })];
93
+ case 1:
94
+ response = _d.sent();
95
+ _c = (response === null || response === void 0 ? void 0 : response.data) || {}, code = _c.code, data = _c.data, msg = _c.msg;
96
+ if (code === 200 && data) {
97
+ return [2 /*return*/, {
98
+ apiAesKey: data.apiAesKey,
99
+ apiDesKey: data.apiDesKey,
100
+ ossAesKey: data.ossAesKey,
101
+ ossDesKey: data.ossDesKey,
102
+ }];
103
+ }
104
+ throw new Error(msg || "获取安全密钥失败");
105
+ }
106
+ });
107
+ });
108
+ }
109
+ /**
110
+ * 从window旧密钥创建Crypto实例 兼容未改造的应用
111
+ */
112
+ function createCryptoFromWindow() {
113
+ var aesKey = window.__CRYPTO_AES_KEY__;
114
+ var desKey = window.__CRYPTO_DES_KEY__;
115
+ if (aesKey) {
116
+ return new Crypto(aesKey, desKey);
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ export { getApiCrypto, getOssCrypto, initSecurityKeys, refreshSecurityKeys };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { getPreToken, getToken, parseToken, removeToken, setToken } from './es/a
5
5
  export { AxiosRequest, IOptions, IOtherOptions } from './es/request.js';
6
6
  export { getBaseUrl } from './es/baseUrl.js';
7
7
  export { Crypto, md5 } from './es/crypto.js';
8
+ export { ISecurityKeys, getApiCrypto, getOssCrypto, initSecurityKeys, refreshSecurityKeys } from './es/securityKey.js';
8
9
  export { addThousedSeparator, appendQueryParamsToUrl, copyToClipboard, dangerouslyXss, delay, emitter, getFileExtension, isChinese, isImageExtension, isNonEmpty, parseJoin, parseQueryParams, specialString, unescapeString } from './es/common.js';
9
10
  export { divide, exactRound, formatUnit, minus, plus, times } from './es/math.js';
10
11
  export { validatePassword } from './es/passwordValidate.js';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { getPreToken, getToken, parseToken, removeToken, setToken } from './es/a
5
5
  export { AxiosRequest } from './es/request.js';
6
6
  export { getBaseUrl } from './es/baseUrl.js';
7
7
  export { Crypto, md5 } from './es/crypto.js';
8
+ export { getApiCrypto, getOssCrypto, initSecurityKeys, refreshSecurityKeys } from './es/securityKey.js';
8
9
  export { addThousedSeparator, appendQueryParamsToUrl, copyToClipboard, dangerouslyXss, delay, emitter, getFileExtension, isChinese, isImageExtension, isNonEmpty, parseJoin, parseQueryParams, specialString, unescapeString } from './es/common.js';
9
10
  export { divide, exactRound, formatUnit, minus, plus, times } from './es/math.js';
10
11
  export { validatePassword } from './es/passwordValidate.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zmdms-utils",
3
- "version": "0.0.96",
3
+ "version": "0.0.98",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
package/src/file.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  FILE_PRVIEW_SERVICE_URL,
16
16
  } from "./constants";
17
17
  import { AxiosResponse } from "axios";
18
- import { Crypto } from "./crypto";
18
+ import { getOssCrypto } from "./securityKey";
19
19
 
20
20
  // 是否是中拓的附件服务
21
21
  const isZmdmsFileService = FILE_SERVICE === "zmdms";
@@ -546,10 +546,12 @@ interface IThumbnailLinkOptions extends Partial<IOptions> {
546
546
  * 3. 如果参数是通过body传输的,只需要encode一次
547
547
  */
548
548
  export function encodeFileId(fileId: string, encodeNum: number = 1) {
549
- const crypto = new Crypto(
550
- (window as any).__CRYPTO_AES_KEY__,
551
- (window as any).__CRYPTO_DES_KEY__,
552
- );
549
+ const crypto = getOssCrypto();
550
+ if (!crypto) {
551
+ throw new Error(
552
+ "缺少附件加密密钥,请先调用initSecurityKeys初始化或设置window.__CRYPTO_AES_KEY__",
553
+ );
554
+ }
553
555
  if (encodeNum === 0) {
554
556
  return crypto.encrypt(fileId);
555
557
  }
package/src/index.ts CHANGED
@@ -11,6 +11,13 @@ export {
11
11
  export { AxiosRequest, type IOtherOptions, type IOptions } from "./request";
12
12
  export { getBaseUrl } from "./baseUrl";
13
13
  export { Crypto, md5, Base64 } from "./crypto";
14
+ export {
15
+ initSecurityKeys,
16
+ refreshSecurityKeys,
17
+ getApiCrypto,
18
+ getOssCrypto,
19
+ type ISecurityKeys,
20
+ } from "./securityKey";
14
21
  export {
15
22
  delay,
16
23
  emitter,
package/src/request.ts CHANGED
@@ -7,6 +7,7 @@ import axios, {
7
7
  } from "axios";
8
8
  import { getPreToken, removeToken } from "./auth";
9
9
  import { Crypto } from "./crypto";
10
+ import { initSecurityKeys, getApiCrypto } from "./securityKey";
10
11
  import { TOKEN_KEY } from "./constants";
11
12
 
12
13
  const defaultOptions: IOtherOptions = {
@@ -286,7 +287,7 @@ export class AxiosRequest {
286
287
  }
287
288
 
288
289
  // 实例方法
289
- public request(options: IOptions) {
290
+ public async request(options: IOptions) {
290
291
  const { isFormData, isAuth = true, ...resetOptions } = options;
291
292
 
292
293
  const newOptions = resetOptions;
@@ -300,6 +301,20 @@ export class AxiosRequest {
300
301
  if (token) {
301
302
  newOptions.headers[this.tokenKey] = token;
302
303
  }
304
+ // 自动密钥模式:带token的请求顺带预热密钥 不阻塞当前请求
305
+ // 预热失败静默处理 真正需要密钥的请求会再走到等待逻辑并报错
306
+ if (this.otherOptions.securityKey) {
307
+ initSecurityKeys({
308
+ baseURL: this.getSecurityKeyBaseURL(options),
309
+ }).catch(() => {});
310
+ }
311
+ }
312
+
313
+ // 自动密钥模式:加密请求优先等待密钥就绪(外部已传入crypto实例时除外)
314
+ if (newOptions.isCrypto && !this.crypto && this.otherOptions.securityKey) {
315
+ await initSecurityKeys({
316
+ baseURL: this.getSecurityKeyBaseURL(options),
317
+ });
303
318
  }
304
319
 
305
320
  // 遍历params,将undefined值 转为null值
@@ -335,15 +350,17 @@ export class AxiosRequest {
335
350
  }
336
351
 
337
352
  // 数据加密处理 TODO:
353
+ // 优先使用外部传入的crypto实例 自动密钥模式下使用密钥管理器的实例
354
+ const crypto =
355
+ this.crypto ||
356
+ (this.otherOptions.securityKey ? getApiCrypto() : undefined);
338
357
  // 将参数进行aes加密
339
358
  if (newOptions.isCrypto === true || newOptions.isCrypto === "aes") {
340
359
  if (newOptions.data) {
341
- newOptions.data = this.crypto?.encrypt?.(
342
- JSON.stringify(newOptions.data),
343
- );
360
+ newOptions.data = crypto?.encrypt?.(JSON.stringify(newOptions.data));
344
361
  }
345
362
  if (newOptions.params) {
346
- newOptions.params = this.crypto?.encrypt?.(
363
+ newOptions.params = crypto?.encrypt?.(
347
364
  JSON.stringify(newOptions.params),
348
365
  );
349
366
  }
@@ -351,12 +368,12 @@ export class AxiosRequest {
351
368
  // 将参数进行des加密
352
369
  if (newOptions.isCrypto === "des") {
353
370
  if (newOptions.data) {
354
- newOptions.data = this.crypto?.encrypt_des?.(
371
+ newOptions.data = crypto?.encrypt_des?.(
355
372
  JSON.stringify(newOptions.data),
356
373
  );
357
374
  }
358
375
  if (newOptions.params) {
359
- newOptions.params = this.crypto?.encrypt_des?.(
376
+ newOptions.params = crypto?.encrypt_des?.(
360
377
  JSON.stringify(newOptions.params),
361
378
  );
362
379
  }
@@ -364,6 +381,22 @@ export class AxiosRequest {
364
381
 
365
382
  return this.instanceAxios(newOptions);
366
383
  }
384
+
385
+ /**
386
+ * 推断密钥接口的请求前缀地址
387
+ * 绝对地址的请求取其origin 其他情况取实例的baseURL(同源场景为空串)
388
+ */
389
+ private getSecurityKeyBaseURL(options: IOptions): string {
390
+ const url = options.url || "";
391
+ if (/^https?:\/\//i.test(url)) {
392
+ try {
393
+ return new URL(url).origin;
394
+ } catch (err) {
395
+ console.log(err);
396
+ }
397
+ }
398
+ return this.otherOptions.baseURL || "";
399
+ }
367
400
  }
368
401
 
369
402
  function transformData(data?: any): any {
@@ -442,6 +475,12 @@ export interface IOtherOptions {
442
475
  baseURL?: string;
443
476
  /** 加密实例方法 */
444
477
  crypto?: Crypto;
478
+ /**
479
+ * 开启自动密钥模式
480
+ * 开启后无需传crypto 加密请求会自动等待密钥就绪后加密
481
+ * 带token的请求会顺带预热密钥
482
+ */
483
+ securityKey?: boolean;
445
484
  /** 超时是否需要弹框 */
446
485
  isTimeoutConfirm?: boolean;
447
486
  /** 超时弹框提示内容 */
@@ -0,0 +1,151 @@
1
+ /**
2
+ * 安全密钥管理
3
+ * 从后端接口获取前端加解密密钥,替代打包时的硬编码密钥,避免密钥随JS泄露
4
+ */
5
+ import axios from "axios";
6
+ import { getPreToken } from "./auth";
7
+ import { BASIC_KEY, TOKEN_KEY } from "./constants";
8
+ import { Crypto } from "./crypto";
9
+
10
+ /**
11
+ * 密钥集合
12
+ */
13
+ export interface ISecurityKeys {
14
+ /** 接口请求参数加密用 AES 密钥 */
15
+ apiAesKey: string;
16
+ /** 接口请求参数加密用 DES 密钥 */
17
+ apiDesKey: string;
18
+ /** 附件(OSS)相关加密用 AES 密钥 */
19
+ ossAesKey: string;
20
+ /** 附件(OSS)相关加密用 DES 密钥 */
21
+ ossDesKey: string;
22
+ }
23
+
24
+ interface ISecurityKeysOptions {
25
+ /** 接口请求的前缀地址 必传 */
26
+ baseURL: string;
27
+ }
28
+
29
+ // 密钥内存缓存 不挂载到window 避免产生新的明文全局变量
30
+ let securityKeys: ISecurityKeys | null = null;
31
+ // 进行中的密钥请求 并发调用initSecurityKeys/refreshSecurityKeys时共享同一个请求
32
+ let securityKeysPromise: Promise<ISecurityKeys> | null = null;
33
+
34
+ /**
35
+ * 初始化安全密钥
36
+ * 应用启动时(render之前)调用一次 并发调用会共享同一个请求
37
+ * @param options
38
+ * @param options.baseURL {string} 接口请求的前缀地址 必传
39
+ * @returns 密钥集合
40
+ */
41
+ export function initSecurityKeys(
42
+ options: ISecurityKeysOptions,
43
+ ): Promise<ISecurityKeys> {
44
+ if (securityKeys) {
45
+ return Promise.resolve(securityKeys);
46
+ }
47
+ if (securityKeysPromise) {
48
+ return securityKeysPromise;
49
+ }
50
+ return requestSecurityKeys(options);
51
+ }
52
+
53
+ /**
54
+ * 强制刷新安全密钥
55
+ * 后端轮换密钥后可调用 会忽略已有缓存重新请求
56
+ * @param options
57
+ * @param options.baseURL {string} 接口请求的前缀地址 必传
58
+ * @returns 密钥集合
59
+ */
60
+ export function refreshSecurityKeys(
61
+ options: ISecurityKeysOptions,
62
+ ): Promise<ISecurityKeys> {
63
+ return requestSecurityKeys(options);
64
+ }
65
+
66
+ /**
67
+ * 获取接口请求参数加密的Crypto实例
68
+ * 未初始化时回退读取window上的旧密钥(兼容未接入initSecurityKeys的应用)
69
+ * @returns Crypto实例 未获取到密钥时返回undefined
70
+ */
71
+ export function getApiCrypto(): Crypto | undefined {
72
+ if (securityKeys?.apiAesKey) {
73
+ return new Crypto(securityKeys.apiAesKey, securityKeys.apiDesKey);
74
+ }
75
+ return createCryptoFromWindow();
76
+ }
77
+
78
+ /**
79
+ * 获取附件(OSS)相关加密的Crypto实例
80
+ * 未初始化时回退读取window上的旧密钥(兼容未接入initSecurityKeys的应用)
81
+ * @returns Crypto实例 未获取到密钥时返回undefined
82
+ */
83
+ export function getOssCrypto(): Crypto | undefined {
84
+ if (securityKeys?.ossAesKey) {
85
+ return new Crypto(securityKeys.ossAesKey, securityKeys.ossDesKey);
86
+ }
87
+ return createCryptoFromWindow();
88
+ }
89
+
90
+ /**
91
+ * 请求密钥并缓存
92
+ */
93
+ function requestSecurityKeys(
94
+ options: ISecurityKeysOptions,
95
+ ): Promise<ISecurityKeys> {
96
+ securityKeysPromise = fetchSecurityKeys(options).then(
97
+ (keys) => {
98
+ securityKeys = keys;
99
+ securityKeysPromise = null;
100
+ return keys;
101
+ },
102
+ (err) => {
103
+ securityKeysPromise = null;
104
+ throw err;
105
+ },
106
+ );
107
+ return securityKeysPromise;
108
+ }
109
+
110
+ /**
111
+ * 请求密钥接口
112
+ * 使用裸axios请求(不走AxiosRequest) 避免请求本身依赖密钥或触发统一的异常处理
113
+ */
114
+ async function fetchSecurityKeys(
115
+ options: ISecurityKeysOptions,
116
+ ): Promise<ISecurityKeys> {
117
+ const apiBaseURL = options.baseURL?.endsWith?.("/")
118
+ ? options.baseURL.slice(0, options.baseURL.length - 1)
119
+ : options.baseURL;
120
+ const headers: { [prop: string]: string } = {};
121
+ const token = getPreToken();
122
+ if (token) {
123
+ headers[TOKEN_KEY] = token;
124
+ }
125
+ const response = await axios.get(
126
+ `${apiBaseURL}/api/${BASIC_KEY}-resource/resource/get-security-key`,
127
+ { headers, timeout: 60000 },
128
+ );
129
+ const { code, data, msg } = response?.data || {};
130
+ if (code === 200 && data) {
131
+ return {
132
+ apiAesKey: data.apiAesKey,
133
+ apiDesKey: data.apiDesKey,
134
+ ossAesKey: data.ossAesKey,
135
+ ossDesKey: data.ossDesKey,
136
+ };
137
+ }
138
+ throw new Error(msg || "获取安全密钥失败");
139
+ }
140
+
141
+ /**
142
+ * 从window旧密钥创建Crypto实例 兼容未改造的应用
143
+ */
144
+ function createCryptoFromWindow(): Crypto | undefined {
145
+ const aesKey = (window as any).__CRYPTO_AES_KEY__;
146
+ const desKey = (window as any).__CRYPTO_DES_KEY__;
147
+ if (aesKey) {
148
+ return new Crypto(aesKey, desKey);
149
+ }
150
+ return undefined;
151
+ }