Skip to content

Commit 68058cc

Browse files
committed
【feature】新增加密隧道工具; review by songym
1 parent affe702 commit 68058cc

File tree

9 files changed

+400
-82
lines changed

9 files changed

+400
-82
lines changed

build/webpack.config.base.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ module.exports = {
5858

5959
//其它解决方案配置
6060
resolve: {
61-
extensions: ['.js', '.json', '.css']
61+
extensions: ['.js', '.json', '.css'],
62+
fallback: {
63+
crypto: false,
64+
stream: false,
65+
vm: false
66+
}
6267
},
6368

6469
externals: {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
"mapbox-gl": "1.13.2",
154154
"maplibre-gl": "3.1.0",
155155
"mapv": "2.0.62",
156+
"node-forge": "^1.3.1",
156157
"ol": "6.14.1",
157158
"pbf": "3.2.1",
158159
"process": "^0.11.10",

src/common/index.common.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,8 @@ import {
333333
ArrayStatistic,
334334
getMeterPerMapUnit,
335335
getWrapNum,
336-
conversionDegree
336+
conversionDegree,
337+
EncryptFetchRequestUtil
337338
} from './util';
338339
import { CartoCSS, ThemeStyle } from './style';
339340
import {
@@ -498,6 +499,7 @@ export {
498499
isCORS,
499500
setCORS,
500501
FetchRequest,
502+
EncryptFetchRequestUtil,
501503
ColorsPickerUtil,
502504
ArrayStatistic,
503505
getMeterPerMapUnit,

src/common/namespace.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ import {
337337
isCORS,
338338
setCORS,
339339
FetchRequest,
340+
EncryptFetchRequestUtil,
340341
ColorsPickerUtil,
341342
ArrayStatistic,
342343
CartoCSS,
@@ -493,6 +494,7 @@ SuperMap.isCORS = isCORS;
493494
SuperMap.setRequestTimeout = setRequestTimeout;
494495
SuperMap.getRequestTimeout = getRequestTimeout;
495496
SuperMap.FetchRequest = FetchRequest;
497+
SuperMap.EncryptFetchRequestUtil = EncryptFetchRequestUtil;
496498

497499
// commontypes
498500
SuperMap.inherit = inheritExt;

src/common/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
"echarts": "5.4.3",
1919
"fetch-ie8": "1.5.0",
2020
"fetch-jsonp": "1.1.3",
21+
"flatgeobuf": "3.23.1",
2122
"promise-polyfill": "8.2.3",
2223
"lodash.topairs": "4.3.0",
2324
"lodash.uniqby": "^4.7.0",
2425
"lodash.clonedeep": "^4.5.0",
2526
"mapv": "2.0.62",
26-
"flatgeobuf": "3.23.1",
27+
"node-forge": "^1.3.1",
2728
"rbush": "^2.0.2",
2829
"urijs": "^1.19.11",
2930
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { FetchRequest } from './FetchRequest';
2+
import {
3+
RSAEncrypt,
4+
AESGCMEncrypt,
5+
AESGCMDecrypt,
6+
generateAESRandomKey,
7+
generateAESRandomIV
8+
} from './RSAAndAESEn-DecryptorUtil';
9+
10+
/**
11+
* @private
12+
* @name EncryptFetchRequestUtil
13+
* @namespace
14+
* @category BaseTypes Util
15+
* @classdesc 加密请求地址
16+
*/
17+
export class EncryptFetchRequestUtil {
18+
constructor(serverUrl = 'http://172.16.13.234:8090/iserver') {
19+
this.serverUrl = serverUrl.split('').slice(-1)[0] === '/' ? serverUrl : `${serverUrl}/`;
20+
this.tunnelUrl = undefined;
21+
this.blockedUrlRegex = {
22+
HEAD: [],
23+
POST: [],
24+
GET: [],
25+
PUT: [],
26+
DELETE: []
27+
};
28+
this.encryptAESKey = generateAESRandomKey();
29+
this.encryptAESIV = generateAESRandomIV();
30+
}
31+
32+
async encryptRequest(options) {
33+
const config = Object.assign({ baseURL: '' }, options);
34+
const tunnelUrl = await this.createTunnel();
35+
if (!tunnelUrl) {
36+
return;
37+
}
38+
for (const pattern of this.blockedUrlRegex[config.method.toUpperCase()]) {
39+
const regex = new RegExp(pattern);
40+
if (regex.test(config.baseURL + config.url)) {
41+
const data = {
42+
url: config.baseURL + (config.url.startsWith('/') ? config.url.substring(1, config.url.length) : config.url),
43+
method: config.method,
44+
timeout: config.timeout,
45+
headers: config.headers,
46+
body: config.data
47+
};
48+
// 替换请求
49+
config.method = 'post';
50+
config.data = AESGCMEncrypt(this.encryptAESKey, this.encryptAESIV, JSON.stringify(data));
51+
if (!config.data) {
52+
throw 'encrypt failed';
53+
}
54+
config.url = this.tunnelUrl;
55+
break;
56+
}
57+
}
58+
const response = await FetchRequest.commit(config.method, config.url, config.data, config.options);
59+
if (config.url === this.tunnelUrl) {
60+
const result = await response.text();
61+
const decryptResult = AESGCMDecrypt(this.encryptAESKey, this.encryptAESIV, result);
62+
if (!decryptResult) {
63+
console.debug('解密请求响应失败');
64+
return;
65+
}
66+
return JSON.parse(decryptResult).data;
67+
}
68+
return response;
69+
}
70+
71+
async getRSAPublicKey() {
72+
try {
73+
const response = await FetchRequest.get(`${this.serverUrl}services/security/tunnel/v1/publickey`);
74+
// 解析publicKey
75+
const publicKeyObj = await response.json();
76+
// 生成AES密钥
77+
const aesKeyObj = {
78+
key: this.encryptAESKey,
79+
iv: this.encryptAESIV,
80+
mode: 'GCM',
81+
padding: 'NoPadding'
82+
};
83+
// 将AES密钥使用RSA公钥加密
84+
const aesCipherText = RSAEncrypt(publicKeyObj.publicKey, aesKeyObj.key + aesKeyObj.iv);
85+
return aesCipherText;
86+
} catch (error) {
87+
console.debug('RSA公钥获取失败,错误详情:' + error);
88+
}
89+
}
90+
91+
async createTunnel() {
92+
if (!this.tunnelUrl) {
93+
try {
94+
const data = await this.getRSAPublicKey();
95+
if (!data) {
96+
throw 'fetch RSA publicKey failed';
97+
}
98+
// 创建隧道
99+
const response = await FetchRequest.post(`${this.serverUrl}services/security/tunnel/v1/tunnels`, data);
100+
const result = await response.json();
101+
Object.assign(this, {
102+
tunnelUrl: result.tunnelUrl,
103+
blockedUrlRegex: Object.assign({}, this.blockedUrlRegex, result.blockedUrlRegex)
104+
});
105+
} catch (error) {
106+
console.debug('安全隧道创建失败,错误详情:' + error);
107+
}
108+
}
109+
return this.tunnelUrl;
110+
}
111+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import pki from 'node-forge/lib/pki';
2+
import md from 'node-forge/lib/md';
3+
import cipher from 'node-forge/lib/cipher';
4+
import util from 'node-forge/lib/util';
5+
6+
/**
7+
* @private
8+
* @function RSAEncrypt
9+
* @description RSAES-OAEP/SHA-256/MGF1-SHA-1加密,对应java的RSA/ECB/OAEPWithSHA-256AndMGF1Padding
10+
* @param publicKeyStr - RSA 公钥
11+
* @param message - 需要加密的信息
12+
* @returns {string|boolean} 加密成功返回base64编码的密文,加密失败返回false
13+
*/
14+
export function RSAEncrypt (publicKeyStr, message) {
15+
if (publicKeyStr && publicKeyStr.indexOf('BEGIN PUBLIC KEY') === -1) { // 转为PEM格式
16+
publicKeyStr = `-----BEGIN PUBLIC KEY-----\n${publicKeyStr}\n-----END PUBLIC KEY-----`;
17+
}
18+
const publicKey = pki.publicKeyFromPem(publicKeyStr);
19+
const obj = {
20+
md: md.sha256.create(),
21+
mgf1: {
22+
md: md.sha1.create()
23+
}
24+
};
25+
const encrypted = publicKey.encrypt(message, 'RSA-OAEP', obj);
26+
if (!encrypted) {
27+
return false; // 加密失败
28+
}
29+
return window.btoa(encrypted);
30+
}
31+
32+
/**
33+
* @private
34+
* @function AESGCMDecrypt
35+
* @description AES/GCM解密
36+
* @param key - 16位
37+
* @param iv - 12位
38+
* @param cipherText - 密文 = base64转码(加密内容 + 16位的mac值)
39+
* @returns {boolean|string} 解密成功返回明文,解密失败返回false
40+
*/
41+
export function AESGCMDecrypt (key, iv, cipherText) {
42+
const cipherStrAndMac = window.atob(cipherText);
43+
const cipherStr = cipherStrAndMac.substring(0, cipherStrAndMac.length - 16);
44+
const mac = cipherStrAndMac.substring(cipherStrAndMac.length - 16);
45+
const decipher = cipher.createDecipher('AES-GCM', util.createBuffer(key));
46+
decipher.start({
47+
iv: util.createBuffer(iv),
48+
additionalData: '', // optional
49+
tagLength: 128, // optional, defaults to 128 bits
50+
tag: mac // authentication tag from encryption
51+
});
52+
decipher.update(util.createBuffer(cipherStr));
53+
const pass = decipher.finish();
54+
if (pass) {
55+
return util.decodeUtf8(decipher.output.data);
56+
}
57+
return false;
58+
}
59+
60+
/**
61+
* @private
62+
* @function AESGCMEncrypt
63+
* @description AES/GCM加密
64+
* @param key - 16位
65+
* @param iv - 12位
66+
* @param msg - 明文
67+
* @returns {boolean|string} 加密成功返回明文,加密失败返回false
68+
*/
69+
export function AESGCMEncrypt (key, iv, msg) {
70+
msg = util.encodeUtf8(msg);
71+
const cipherInstance = cipher.createCipher('AES-GCM', key);
72+
cipherInstance.start({
73+
iv: iv,
74+
additionalData: '', // 'binary-encoded string', // optional
75+
tagLength: 128 // optional, defaults to 128 bits
76+
});
77+
cipherInstance.update(util.createBuffer(msg));
78+
const pass = cipherInstance.finish();
79+
if (pass) {
80+
const encrypted = cipherInstance.output;
81+
const tag = cipherInstance.mode.tag;
82+
return window.btoa(encrypted.data + tag.data);
83+
}
84+
return false;
85+
}
86+
87+
/**
88+
* @private
89+
* @function generateAESRandomKey
90+
* @description 生成随机的16位 AES key
91+
* @returns {string}
92+
*/
93+
export function generateAESRandomKey () {
94+
return randomString(16);
95+
}
96+
97+
/**
98+
* @private
99+
* @function generateAESRandomIV
100+
* @description 生成随机的12位 AES iv
101+
* @returns {string}
102+
*/
103+
export function generateAESRandomIV () {
104+
return randomString(12);
105+
}
106+
107+
/**
108+
* @private
109+
* @function randomString
110+
* @description 生成指定长度的随机字符串
111+
* @param length - 随机字符串长度
112+
* @returns {string}
113+
*/
114+
function randomString (length) {
115+
const str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
116+
let result = '';
117+
for (let i = length; i > 0; --i) { result += str[Math.floor(Math.random() * str.length)]; }
118+
return result;
119+
}

0 commit comments

Comments
 (0)