Files
sub2apipay/src/lib/easy-pay/client.ts
erio 4ce3484179 fix: 全面安全审计修复 — 支付验签、IDOR、竞态、token过期等
- H1: 支付宝响应验签 (verifyResponseSign + bracket-matching 提取签名内容)
- H2/H3: EasyPay queryOrder 从 GET 改 POST,PKEY 不再暴露于 URL
- H5: users/[id] IDOR 修复,校验当前用户只能查询自身信息
- H6: 限额校验移入 prisma.$transaction() 防止 TOCTOU 竞态
- C1: access_token 增加 24h 过期、userId 绑定、派生密钥分离
- M1: EasyPay 回调增加 pid 校验防跨商户注入
- M4: 充值码增加 crypto.randomBytes 随机后缀
- M5: 过期订单批量处理增加 BATCH_SIZE 限制
- M6: 退款失败增加 [CRITICAL] 日志和余额补偿标记
- M7: admin channels PUT 增加 Zod schema 校验
- M8: admin subscriptions 分页参数增加上限
- M9: orders src_url 限制 HTTP/HTTPS 协议
- L1: 微信支付回调时间戳 NaN 检查
- L9: WXPAY_API_V3_KEY 长度校验
2026-03-14 04:36:33 +08:00

133 lines
4.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 { EasyPayCreateResponse, EasyPayQueryResponse, EasyPayRefundResponse } from './types';
export interface CreatePaymentOptions {
outTradeNo: string;
amount: string;
paymentType: string;
clientIp: string;
productName: string;
returnUrl?: string;
}
function normalizeCidList(cid?: string): string | undefined {
if (!cid) return undefined;
const normalized = cid
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.join(',');
return normalized || undefined;
}
function resolveCid(paymentType: string): string | undefined {
const env = getEnv();
if (paymentType === 'alipay') {
return normalizeCidList(env.EASY_PAY_CID_ALIPAY) || normalizeCidList(env.EASY_PAY_CID);
}
return normalizeCidList(env.EASY_PAY_CID_WXPAY) || normalizeCidList(env.EASY_PAY_CID);
}
function assertEasyPayEnv(env: ReturnType<typeof getEnv>) {
if (
!env.EASY_PAY_PID ||
!env.EASY_PAY_PKEY ||
!env.EASY_PAY_API_BASE ||
!env.EASY_PAY_NOTIFY_URL ||
!env.EASY_PAY_RETURN_URL
) {
throw new Error(
'EasyPay environment variables (EASY_PAY_PID, EASY_PAY_PKEY, EASY_PAY_API_BASE, EASY_PAY_NOTIFY_URL, EASY_PAY_RETURN_URL) are required',
);
}
return env as typeof env & {
EASY_PAY_PID: string;
EASY_PAY_PKEY: string;
EASY_PAY_API_BASE: string;
EASY_PAY_NOTIFY_URL: string;
EASY_PAY_RETURN_URL: string;
};
}
export async function createPayment(opts: CreatePaymentOptions): Promise<EasyPayCreateResponse> {
const env = assertEasyPayEnv(getEnv());
const params: Record<string, string> = {
pid: env.EASY_PAY_PID,
type: opts.paymentType,
out_trade_no: opts.outTradeNo,
notify_url: env.EASY_PAY_NOTIFY_URL,
return_url: opts.returnUrl || env.EASY_PAY_RETURN_URL,
name: opts.productName,
money: opts.amount,
clientip: opts.clientIp,
};
const cid = resolveCid(opts.paymentType);
if (cid) {
params.cid = cid;
}
const sign = generateSign(params, env.EASY_PAY_PKEY);
params.sign = sign;
params.sign_type = 'MD5';
const formData = new URLSearchParams(params);
const response = await fetch(`${env.EASY_PAY_API_BASE}/mapi.php`, {
method: 'POST',
body: formData,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
signal: AbortSignal.timeout(10_000),
});
const data = (await response.json()) as EasyPayCreateResponse;
if (data.code !== 1) {
throw new Error(`EasyPay create payment failed: ${data.msg || 'unknown error'}`);
}
return data;
}
export async function queryOrder(outTradeNo: string): Promise<EasyPayQueryResponse> {
const env = assertEasyPayEnv(getEnv());
// 使用 POST 避免密钥暴露在 URL 中URL 会被记录到服务器/CDN 日志)
const params = new URLSearchParams({
act: 'order',
pid: env.EASY_PAY_PID,
key: env.EASY_PAY_PKEY,
out_trade_no: outTradeNo,
});
const response = await fetch(`${env.EASY_PAY_API_BASE}/api.php`, {
method: 'POST',
body: params,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
signal: AbortSignal.timeout(10_000),
});
const data = (await response.json()) as EasyPayQueryResponse;
if (data.code !== 1) {
throw new Error(`EasyPay query order failed: ${data.msg || 'unknown error'}`);
}
return data;
}
export async function refund(tradeNo: string, outTradeNo: string, money: string): Promise<EasyPayRefundResponse> {
const env = assertEasyPayEnv(getEnv());
const params = new URLSearchParams({
pid: env.EASY_PAY_PID,
key: env.EASY_PAY_PKEY,
trade_no: tradeNo,
out_trade_no: outTradeNo,
money,
});
const response = await fetch(`${env.EASY_PAY_API_BASE}/api.php?act=refund`, {
method: 'POST',
body: params,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
signal: AbortSignal.timeout(10_000),
});
const data = (await response.json()) as EasyPayRefundResponse;
if (data.code !== 1) {
throw new Error(`EasyPay refund failed: ${data.msg || 'unknown error'}`);
}
return data;
}