Skip to content

Commit

Permalink
feat: 实现支付宝响应验签
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 committed Jun 7, 2024
1 parent c828db2 commit ea63a50
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 12 deletions.
50 changes: 39 additions & 11 deletions src/alipay.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { debuglog } from 'node:util';
import { createVerify, randomUUID, createSign } from 'node:crypto';
import { createVerify, randomUUID } from 'node:crypto';
import { Readable } from 'node:stream';
import urllib, { Agent } from 'urllib';
import urllib, { Agent, IncomingHttpHeaders } from 'urllib';
import type {
HttpClientResponse, HttpMethod, RequestOptions, RawResponseWithMeta,
} from 'urllib';
Expand All @@ -15,6 +15,7 @@ import {
sign, ALIPAY_ALGORITHM_MAPPING, decamelize, createRequestId, readableToBytes,
aesDecrypt, aesEncryptText,
aesDecryptText,
signatureV3, verifySignatureV3,
} from './util.js';
import { getSNFromPath, getSN, loadPublicKey, loadPublicKeyFromPath } from './antcertutil.js';

Expand All @@ -38,6 +39,7 @@ export interface AlipayRequestErrorOptions extends ErrorOptions {
traceId?: string;
responseHttpStatus?: number;
responseDataRaw?: string;
responseHttpHeaders?: IncomingHttpHeaders;
links?: AlipayRequestErrorSupportLink[];
}

Expand All @@ -46,6 +48,7 @@ export class AlipayRequestError extends Error {
traceId?: string;
responseHttpStatus?: number;
responseDataRaw?: string;
responseHttpHeaders?: IncomingHttpHeaders;
links?: AlipayRequestErrorSupportLink[];

constructor(message: string, options?: AlipayRequestErrorOptions) {
Expand All @@ -56,6 +59,7 @@ export class AlipayRequestError extends Error {
this.code = options?.code;
this.traceId = options?.traceId;
this.responseHttpStatus = options?.responseHttpStatus;
this.responseHttpHeaders = options?.responseHttpHeaders;
this.responseDataRaw = options?.responseDataRaw;
this.links = options?.links;
this.name = this.constructor.name;
Expand Down Expand Up @@ -327,12 +331,18 @@ export class AlipaySdk {
dataType: dataType === 'stream' ? 'stream' : 'text',
timeout: options?.requestTimeout ?? this.config.timeout,
};
// 默认需要对响应做验签,确保响应是由支付宝返回的
let validateResponseSignature = true;
if (dataType === 'stream') {
// 使用 HTTP/2 请求才支持流式响应
requestOptions.dispatcher = http2Agent;
// 流式响应不需要对响应做验签
validateResponseSignature = false;
}

Check warning on line 341 in src/alipay.ts

View check run for this annotation

Codecov / codecov/patch

src/alipay.ts#L339-L341

Added lines #L339 - L341 were not covered by tests
if (validateResponseSignature && !this.config.alipayPublicKey) {
throw new TypeError('请确保支付宝公钥 config.alipayPublicKey 已经配置,需要使用它对响应进行验签');

Check warning on line 343 in src/alipay.ts

View check run for this annotation

Codecov / codecov/patch

src/alipay.ts#L343

Added line #L343 was not covered by tests
}
const requestId = options?.requestId ?? createRequestId();

requestOptions.headers = {
'user-agent': this.version,
'alipay-request-id': requestId,
Expand Down Expand Up @@ -438,11 +448,9 @@ export class AlipaySdk {
requestOptions.headers['alipay-app-auth-token'] = options.appAuthToken;
signString += `${options.appAuthToken}\n`;
}
const signature = createSign('RSA-SHA256')
.update(signString, 'utf-8')
.sign(this.config.privateKey, 'base64');
const signature = signatureV3(signString, this.config.privateKey);
const authorization = `ALIPAY-SHA256withRSA ${authString},sign=${signature}`;
debug('signString: \n--------\n%s\n--------\n, authorization: %o', signString, authorization);
debug('signString: \n--------\n%s\n--------\nauthorization: %o', signString, authorization);
requestOptions.headers.authorization = authorization;
debug('curl %s %s, with body: %s, headers: %j, dataType: %s',
httpMethod, url, httpRequestBody, requestOptions.headers, dataType);
Expand Down Expand Up @@ -479,6 +487,7 @@ export class AlipaySdk {
code: errorData.code,
links: errorData.links,
responseHttpStatus: httpResponse.status,
responseHttpHeaders: httpResponse.headers,
traceId,
});
}
Expand All @@ -490,20 +499,39 @@ export class AlipaySdk {
traceId,
} satisfies AlipayCommonResultStream;
}
let responseDataText = httpResponse.data as string;
let httpResponseBody = httpResponse.data as string;

// 对支付宝响应进行验签 https://opendocs.alipay.com/open-v3/054d0z?pathHash=dcad8d5c
if (validateResponseSignature) {
const headers = httpResponse.headers;
const responseSignString = `${headers['alipay-timestamp']}\n${headers['alipay-nonce']}\n${httpResponseBody}\n`;
const expectedSignature = headers['alipay-signature'] as string;
debug('responseSignString: \n--------\n%s\n--------\nexpectedSignature: %o', responseSignString, expectedSignature);
if (!verifySignatureV3(responseSignString, expectedSignature, this.config.alipayPublicKey)) {
throw new AlipayRequestError(`支付宝响应验签失败,请确保支付宝公钥 config.alipayPublicKey 是最新有效版本,签名字符串为:${expectedSignature},验证字符串为:${JSON.stringify(responseSignString)}`, {
code: 'response-signature-verify-error',
responseDataRaw: httpResponse.data,
responseHttpStatus: httpResponse.status,
responseHttpHeaders: httpResponse.headers,
traceId,
});
}

Check warning on line 518 in src/alipay.ts

View check run for this annotation

Codecov / codecov/patch

src/alipay.ts#L511-L518

Added lines #L511 - L518 were not covered by tests
}

if (options?.needEncrypt) {
responseDataText = aesDecryptText(responseDataText, this.config.encryptKey);
if (!responseDataText) {
httpResponseBody = aesDecryptText(httpResponseBody, this.config.encryptKey);
if (!httpResponseBody) {

Check warning on line 523 in src/alipay.ts

View check run for this annotation

Codecov / codecov/patch

src/alipay.ts#L522-L523

Added lines #L522 - L523 were not covered by tests
throw new AlipayRequestError('解密失败,请确认 config.encryptKey 设置正确', {
code: 'decrypt-error',
responseDataRaw: httpResponse.data,
responseHttpStatus: httpResponse.status,
responseHttpHeaders: httpResponse.headers,

Check warning on line 528 in src/alipay.ts

View check run for this annotation

Codecov / codecov/patch

src/alipay.ts#L528

Added line #L528 was not covered by tests
traceId,
});
}
}
return {
data: JSON.parse(responseDataText),
data: JSON.parse(httpResponseBody),
responseHttpStatus: httpResponse.status,
traceId,
} satisfies AlipayCommonResult<T>;
Expand Down
16 changes: 15 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { debuglog } from 'node:util';
import { createSign, randomUUID } from 'node:crypto';
import { createSign, createVerify, randomUUID } from 'node:crypto';
import type { Readable } from 'node:stream';
import type { ReadableStream } from 'node:stream/web';
import { YYYYMMDDHHmmss } from 'utility';
Expand Down Expand Up @@ -125,6 +125,20 @@ export function sign(method: string, params: Record<string, any>, config: Requir
return decamelizeParams;
}

/** OpenAPI 3.0 签名,使用应用私钥签名 */
export function signatureV3(signString: string, appPrivateKey: string) {
return createSign('RSA-SHA256')
.update(signString, 'utf-8')
.sign(appPrivateKey, 'base64');
}

/** OpenAPI 3.0 验签,使用支付宝公钥验证签名 */
export function verifySignatureV3(signString: string, expectedSignature: string, alipayPublicKey: string) {
return createVerify('RSA-SHA256')
.update(signString, 'utf-8')
.verify(alipayPublicKey, expectedSignature, 'base64');
}

export function createRequestId() {
return randomUUID().replaceAll('-', '');
}
Expand Down

0 comments on commit ea63a50

Please sign in to comment.