Files
sub2apipay/src/lib/alipay/client.ts
erio bdf2577f28 fix: 支付安全审核修复(支付宝+微信)
支付宝:
- 回调增加 app_id 校验,防止跨商户通知
- 回调增加 sign_type 过滤,仅接受 RSA2
- 退款增加 out_request_no 保证幂等
- 金额解析增加精度保护
- timestamp 改用 CST 时区

微信:
- 自行实现 AES-GCM 解密替代库的 decipher_gcm(修复 AuthTag 未验证)
- WXPAY_PUBLIC_KEY_ID 改为必填
- serial 匹配检查改为强制
- 时间戳校验移到签名验证之前
- nonce 改用 crypto.randomBytes
- publicKey 不允许空 Buffer fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:57:55 +08:00

99 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getEnv } from '@/lib/config';
import { generateSign } from './sign';
import type { AlipayResponse } from './types';
const GATEWAY = 'https://openapi.alipay.com/gateway.do';
function getCommonParams(appId: string): Record<string, string> {
return {
app_id: appId,
format: 'JSON',
charset: 'utf-8',
sign_type: 'RSA2',
timestamp: new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' }).replace('T', ' '),
version: '1.0',
};
}
function assertAlipayEnv(env: ReturnType<typeof getEnv>) {
if (!env.ALIPAY_APP_ID || !env.ALIPAY_PRIVATE_KEY || !env.ALIPAY_PUBLIC_KEY) {
throw new Error('Alipay environment variables (ALIPAY_APP_ID, ALIPAY_PRIVATE_KEY, ALIPAY_PUBLIC_KEY) are required');
}
return env as typeof env & {
ALIPAY_APP_ID: string;
ALIPAY_PRIVATE_KEY: string;
ALIPAY_PUBLIC_KEY: string;
};
}
/**
* 生成支付宝网站/H5支付的跳转 URLGET 方式)
* PC: alipay.trade.page.pay H5: alipay.trade.wap.pay
*/
export function pageExecute(
bizContent: Record<string, unknown>,
options?: { notifyUrl?: string; returnUrl?: string; method?: string },
): string {
const env = assertAlipayEnv(getEnv());
const params: Record<string, string> = {
...getCommonParams(env.ALIPAY_APP_ID),
method: options?.method || 'alipay.trade.page.pay',
biz_content: JSON.stringify(bizContent),
};
if (options?.notifyUrl || env.ALIPAY_NOTIFY_URL) {
params.notify_url = (options?.notifyUrl || env.ALIPAY_NOTIFY_URL)!;
}
if (options?.returnUrl || env.ALIPAY_RETURN_URL) {
params.return_url = (options?.returnUrl || env.ALIPAY_RETURN_URL)!;
}
params.sign = generateSign(params, env.ALIPAY_PRIVATE_KEY);
const query = new URLSearchParams(params).toString();
return `${GATEWAY}?${query}`;
}
/**
* 调用支付宝服务端 APIPOST 方式)
* 用于 alipay.trade.query、alipay.trade.refund、alipay.trade.close
*/
export async function execute<T extends AlipayResponse>(
method: string,
bizContent: Record<string, unknown>,
): Promise<T> {
const env = assertAlipayEnv(getEnv());
const params: Record<string, string> = {
...getCommonParams(env.ALIPAY_APP_ID),
method,
biz_content: JSON.stringify(bizContent),
};
params.sign = generateSign(params, env.ALIPAY_PRIVATE_KEY);
const response = await fetch(GATEWAY, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params).toString(),
signal: AbortSignal.timeout(10_000),
});
const data = await response.json();
// 支付宝响应格式:{ "alipay_trade_query_response": { ... }, "sign": "..." }
const responseKey = method.replace(/\./g, '_') + '_response';
const result = data[responseKey] as T;
if (!result) {
throw new Error(`Alipay API error: unexpected response format for ${method}`);
}
if (result.code !== '10000') {
throw new Error(`Alipay API error: [${result.sub_code || result.code}] ${result.sub_msg || result.msg}`);
}
return result;
}