Skip to content

Commit 0b340c7

Browse files
author
弩哥
committed
新增AES加解密功能
1 parent 3108aa3 commit 0b340c7

13 files changed

+562
-74
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ lib/*.js.map
44
coverage/
55
.vscode/
66
lib/*.d.ts
7-
.nyc_output
7+
.nyc_output
8+
.DS_Store

LEGAL.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Legal Disclaimer
2+
3+
Within this source code, the comments in Chinese shall be the original, governing version. Any comment in other languages are for reference only. In the event of any conflict between the Chinese language version comments and other language version comments, the Chinese language version shall prevail.
4+
5+
法律免责声明
6+
7+
关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,40 @@
2323
```
2424
// TypeScript
2525
import AlipaySdk from 'alipay-sdk';
26+
// 普通公钥模式
27+
const alipaySdk = new AlipaySdk({
28+
// 参考下方 SDK 配置
29+
appId: '2016123456789012',
30+
privateKey: fs.readFileSync('./private-key.pem', 'ascii'),
31+
//可设置AES密钥,调用AES加解密相关接口时需要(可选)
32+
encryptKey: '请填写您的AES密钥,例如:aa4BtZ4tspm2wnXLb1ThQA'
33+
});
2634
35+
// 证书模式
2736
const alipaySdk = new AlipaySdk({
2837
// 参考下方 SDK 配置
2938
appId: '2016123456789012',
3039
privateKey: fs.readFileSync('./private-key.pem', 'ascii'),
40+
alipayRootCertPath: path.join(__dirname,'../fixtures/alipayRootCert.crt'),
41+
appCertPath: path.join(__dirname,'../fixtures/appCertPublicKey.crt'),
42+
alipayPublicCertPath: path.join(__dirname,'../fixtures/alipayCertPublicKey_RSA2.crt'),
3143
});
3244
45+
// 无需加密的接口
3346
const result = await alipaySdk.exec('alipay.system.oauth.token', {
3447
grantType: 'authorization_code',
3548
code: 'code',
3649
refreshToken: 'token'
3750
});
51+
52+
// 需要AES加解密的接口
53+
await alipaySdk.exec('alipay.open.auth.app.aes.set', {
54+
bizContent: {
55+
merchantAppId: '2021001170662064'
56+
},
57+
// 自动AES加解密
58+
needEncrypt: true
59+
});
3860
```
3961

4062
## Demo:

lib/alipay.ts

+91-48
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import * as camelcaseKeys from 'camelcase-keys';
1414
import * as snakeCaseKeys from 'snakecase-keys';
1515

1616
import AliPayForm from './form';
17-
import { sign, ALIPAY_ALGORITHM_MAPPING } from './util';
17+
import { sign, ALIPAY_ALGORITHM_MAPPING, aesDecrypt } from './util';
18+
import { getSNFromPath, getSN, loadPublicKey, loadPublicKeyFromPath } from './antcertutil';
1819

1920
const pkg = require('../package.json');
2021

@@ -43,6 +44,26 @@ export interface AlipaySdkConfig {
4344
urllib?: any;
4445
/** 指定private key类型, 默认: PKCS1, PKCS8: PRIVATE KEY, PKCS1: RSA PRIVATE KEY */
4546
keyType?: 'PKCS1' | 'PKCS8';
47+
/** 应用公钥证书文件路径 */
48+
appCertPath?: string;
49+
/** 应用公钥证书文件内容 */
50+
appCertContent?: string | Buffer;
51+
/** 应用公钥证书sn */
52+
appCertSn?: string;
53+
/** 支付宝根证书文件路径 */
54+
alipayRootCertPath?: string;
55+
/** 支付宝根证书文件内容 */
56+
alipayRootCertContent?: string | Buffer;
57+
/** 支付宝根证书sn */
58+
alipayRootCertSn?: string;
59+
/** 支付宝公钥证书文件路径 */
60+
alipayPublicCertPath?: string;
61+
/** 支付宝公钥证书文件内容 */
62+
alipayPublicCertContent?: string | Buffer;
63+
/** 支付宝公钥证书sn */
64+
alipayCertSn?: string;
65+
/** AES密钥,调用AES加解密相关接口时需要 */
66+
encryptKey?: string;
4667
}
4768

4869
export interface AlipaySdkCommonResult {
@@ -55,6 +76,8 @@ export interface AlipaySdkCommonResult {
5576
export interface IRequestParams {
5677
[key: string]: any;
5778
bizContent?: any;
79+
// 自动AES加解密
80+
needEncrypt?:boolean;
5881
}
5982

6083
export interface IRequestOption {
@@ -75,11 +98,26 @@ class AlipaySdk {
7598
if (!config.privateKey) { throw Error('config.privateKey is required'); }
7699

77100
const privateKeyType = config.keyType === 'PKCS8' ? 'PRIVATE KEY' : 'RSA PRIVATE KEY';
78-
config.privateKey = this.formatKey(config.privateKey, privateKeyType);
79-
if (config.alipayPublicKey) {
101+
config.privateKey = this.formatKey(config.privateKey, privateKeyType);
102+
// 普通公钥模式和证书模式二选其一,传入了证书路径或内容认为是证书模式
103+
if (config.appCertPath || config.appCertContent) {
104+
// 证书模式,优先处理传入了证书内容的情况,其次处理传入证书文件路径的情况
105+
// 应用公钥证书序列号提取
106+
config.appCertSn = is.empty(config.appCertContent) ? getSNFromPath(config.appCertPath, false)
107+
: getSN(config.appCertContent, false);
108+
// 支付宝公钥证书序列号提取
109+
config.alipayCertSn = is.empty(config.alipayPublicCertContent) ? getSNFromPath(config.alipayPublicCertPath, false)
110+
: getSN(config.alipayPublicCertContent, false);
111+
// 支付宝根证书序列号提取
112+
config.alipayRootCertSn = is.empty(config.alipayRootCertContent) ? getSNFromPath(config.alipayRootCertPath, true)
113+
: getSN(config.alipayRootCertContent, true);
114+
config.alipayPublicKey = is.empty(config.alipayPublicCertContent) ? loadPublicKeyFromPath(config.alipayPublicCertPath)
115+
: loadPublicKey(config.alipayPublicCertContent);
116+
config.alipayPublicKey = this.formatKey(config.alipayPublicKey, 'PUBLIC KEY');
117+
} else if (config.alipayPublicKey) {
118+
// 普通公钥模式,传入了支付宝公钥
80119
config.alipayPublicKey = this.formatKey(config.alipayPublicKey, 'PUBLIC KEY');
81120
}
82-
83121
this.config = Object.assign({
84122
urllib,
85123
gateway: 'https://openapi.alipay.com/gateway.do',
@@ -116,12 +154,13 @@ class AlipaySdk {
116154
'app_id', 'method', 'format', 'charset',
117155
'sign_type', 'sign', 'timestamp', 'version',
118156
'notify_url', 'return_url', 'auth_token', 'app_auth_token',
157+
'appCertSn', 'alipayRootCertSn',
119158
];
120159

121160
for (const key in params) {
122161
if (urlArgs.indexOf(key) > -1) {
123162
const val = encodeURIComponent(params[key]);
124-
requestUrl = `${requestUrl}${ requestUrl.includes('?') ? '&' : '?' }${key}=${val}`;
163+
requestUrl = `${requestUrl}${requestUrl.includes('?') ? '&' : '?'}${key}=${val}`;
125164
// 删除 postData 中对应的数据
126165
delete params[key];
127166
}
@@ -135,8 +174,8 @@ class AlipaySdk {
135174
const config = this.config;
136175
let signParams = {} as { [key: string]: string | Object };
137176
let formData = {} as { [key: string]: string | Object | fs.ReadStream };
138-
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null;
139-
const errorLog = (option.log && is.fn(option.log.error)) ? option.log.error : null;
177+
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null;
178+
const errorLog = (option.log && is.fn(option.log.error)) ? option.log.error : null;
140179

141180
option.formData.getFields().forEach((field) => {
142181
// 字段加入签名参数(文件不需要签名)
@@ -171,7 +210,7 @@ class AlipaySdk {
171210
json: false,
172211
timeout: config.timeout,
173212
headers: { 'user-agent': this.sdkVersion },
174-
}, (err, {}, body) => {
213+
}, (err, { }, body) => {
175214
if (err) {
176215
err.message = '[AlipaySdk]exec error';
177216
errorLog && errorLog(err);
@@ -204,7 +243,7 @@ class AlipaySdk {
204243
private pageExec(method: string, option: IRequestOption = {}): Promise<string> {
205244
let signParams = { alipaySdk: this.sdkVersion } as { [key: string]: string | Object };
206245
const config = this.config;
207-
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null;
246+
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null;
208247

209248
option.formData.getFields().forEach((field) => {
210249
signParams[field.name] = field.value;
@@ -248,7 +287,7 @@ class AlipaySdk {
248287

249288
// 消息验签
250289
private notifyRSACheck(signArgs: { [key: string]: any }, signStr: string, signType: 'RSA' | 'RSA2') {
251-
const signContent = Object.keys(signArgs).sort().filter(val => val).map((key) => {
290+
const signContent = Object.keys(signArgs).sort().filter(val => val).map((key) => {
252291
let value = signArgs[key];
253292

254293
if (Array.prototype.toString.call(value) !== '[object String]') {
@@ -334,8 +373,8 @@ class AlipaySdk {
334373
// 计算签名
335374
const signData = sign(method, params, config);
336375
const { url, execParams } = this.formatUrl(config.gateway, signData);
337-
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null;
338-
const errorLog = (option.log && is.fn(option.log.error)) ? option.log.error : null;
376+
const infoLog = (option.log && is.fn(option.log.info)) ? option.log.info : null;
377+
const errorLog = (option.log && is.fn(option.log.error)) ? option.log.error : null;
339378

340379
infoLog && infoLog('[AlipaySdk]start exec, url: %s, method: %s, params: %s',
341380
url, method, JSON.stringify(execParams));
@@ -349,46 +388,50 @@ class AlipaySdk {
349388
timeout: config.timeout,
350389
headers: { 'user-agent': this.sdkVersion },
351390
})
352-
.then((ret: { status: number, data: string }) => {
353-
infoLog && infoLog('[AlipaySdk]exec response: %s', ret);
354-
355-
if (ret.status === 200) {
356-
/**
357-
* 示例响应格式
358-
* {"alipay_trade_precreate_response":
359-
* {"code": "10000","msg": "Success","out_trade_no": "111111","qr_code": "https:\/\/"},
360-
* "sign": "abcde="
361-
* }
362-
* 或者
363-
* {"error_response":
364-
* {"code":"40002","msg":"Invalid Arguments","sub_code":"isv.code-invalid","sub_msg":"授权码code无效"},
365-
* }
366-
*/
367-
const result = JSON.parse(ret.data);
368-
const responseKey = `${method.replace(/\./g, '_')}_response`;
369-
const data = result[responseKey];
370-
371-
if (data) {
372-
// 按字符串验签
373-
const validateSuccess = option.validateSign ? this.checkResponseSign(ret.data, responseKey) : true;
374-
375-
if (validateSuccess) {
376-
resolve(config.camelcase ? camelcaseKeys(data, { deep: true }) : data);
377-
} else {
378-
reject({ serverResult: ret, errorMessage: '[AlipaySdk]验签失败' });
391+
.then((ret: { status: number, data: string }) => {
392+
infoLog && infoLog('[AlipaySdk]exec response: %s', ret);
393+
394+
if (ret.status === 200) {
395+
/**
396+
* 示例响应格式
397+
* {"alipay_trade_precreate_response":
398+
* {"code": "10000","msg": "Success","out_trade_no": "111111","qr_code": "https:\/\/"},
399+
* "sign": "abcde="
400+
* }
401+
* 或者
402+
* {"error_response":
403+
* {"code":"40002","msg":"Invalid Arguments","sub_code":"isv.code-invalid","sub_msg":"授权码code无效"},
404+
* }
405+
*/
406+
const result = JSON.parse(ret.data);
407+
const responseKey = `${method.replace(/\./g, '_')}_response`;
408+
let data = result[responseKey];
409+
410+
if (data) {
411+
if (params.needEncrypt) {
412+
data = aesDecrypt(data, config.encryptKey);
413+
}
414+
415+
// 按字符串验签
416+
const validateSuccess = option.validateSign ? this.checkResponseSign(ret.data, responseKey) : true;
417+
418+
if (validateSuccess) {
419+
resolve(config.camelcase ? camelcaseKeys(data, { deep: true }) : data);
420+
} else {
421+
reject({ serverResult: ret, errorMessage: '[AlipaySdk]验签失败' });
422+
}
379423
}
424+
425+
reject({ serverResult: ret, errorMessage: '[AlipaySdk]HTTP 请求错误' });
380426
}
381427

382428
reject({ serverResult: ret, errorMessage: '[AlipaySdk]HTTP 请求错误' });
383-
}
384-
385-
reject({ serverResult: ret, errorMessage: '[AlipaySdk]HTTP 请求错误' });
386-
})
387-
.catch((err) => {
388-
err.message = '[AlipaySdk]exec error';
389-
errorLog && errorLog(err);
390-
reject(err);
391-
});
429+
})
430+
.catch((err) => {
431+
err.message = '[AlipaySdk]exec error';
432+
errorLog && errorLog(err);
433+
reject(err);
434+
});
392435
});
393436
}
394437

lib/antcertutil.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @author yisheng.cl
3+
* @email [[email protected]]
4+
*/
5+
6+
import * as fs from 'fs';
7+
import bignumber_js_1 from 'bignumber.js';
8+
import * as crypto from 'crypto';
9+
const x509_1 = require('@fidm/x509');
10+
/** 从公钥证书文件里读取支付宝公钥 */
11+
function loadPublicKeyFromPath(filePath: string): string {
12+
const fileData = fs.readFileSync(filePath);
13+
const certificate = x509_1.Certificate.fromPEM(fileData);
14+
return certificate.publicKeyRaw.toString('base64');
15+
}
16+
/** 从公钥证书内容或buffer读取支付宝公钥 */
17+
function loadPublicKey(content: string|Buffer): string {
18+
if (typeof content == 'string') {
19+
content = Buffer.from(content);
20+
}
21+
const certificate = x509_1.Certificate.fromPEM(content);
22+
return certificate.publicKeyRaw.toString('base64');
23+
}
24+
/** 从证书文件里读取序列号 */
25+
function getSNFromPath(filePath: string, isRoot: boolean= false): string {
26+
const fileData = fs.readFileSync(filePath);
27+
return getSN(fileData, isRoot);
28+
}
29+
/** 从上传的证书内容或Buffer读取序列号 */
30+
function getSN(fileData: string|Buffer, isRoot: boolean= false): string {
31+
if (typeof fileData == 'string') {
32+
fileData = Buffer.from(fileData);
33+
}
34+
if (isRoot) {
35+
return getRootCertSN(fileData);
36+
}
37+
const certificate = x509_1.Certificate.fromPEM(fileData);
38+
return getCertSN(certificate);
39+
}
40+
/** 读取序列号 */
41+
function getCertSN(certificate: any): string {
42+
const { issuer, serialNumber } = certificate;
43+
const principalName = issuer.attributes
44+
.reduceRight((prev, curr) => {
45+
const { shortName, value } = curr;
46+
const result = `${prev}${shortName}=${value},`;
47+
return result;
48+
}, '')
49+
.slice(0, -1);
50+
const decimalNumber = new bignumber_js_1(serialNumber, 16).toString(10);
51+
const SN = crypto
52+
.createHash('md5')
53+
.update(principalName + decimalNumber, 'utf8')
54+
.digest('hex');
55+
return SN;
56+
}
57+
/** 读取根证书序列号 */
58+
function getRootCertSN(rootContent: Buffer): string {
59+
const certificates = x509_1.Certificate.fromPEMs(rootContent);
60+
let rootCertSN = '';
61+
certificates.forEach((item) => {
62+
if (item.signatureOID.startsWith('1.2.840.113549.1.1')) {
63+
const SN = getCertSN(item);
64+
if (rootCertSN.length === 0) {
65+
rootCertSN += SN;
66+
}
67+
else {
68+
rootCertSN += `_${SN}`;
69+
}
70+
}
71+
});
72+
return rootCertSN;
73+
}
74+
75+
export {
76+
getSN,
77+
getSNFromPath,
78+
loadPublicKeyFromPath,
79+
loadPublicKey,
80+
};

0 commit comments

Comments
 (0)