如何使用 PHP 接入支付宝自研支付项目?
结论
PHP 接入支付宝支付的流程包括:在支付宝开放平台创建应用并开通支付产品,配置应用密钥或证书,由服务端生成支付请求,接收异步通知并验签,再通过主动查单确认支付结果。
支付密钥只能保存在 PHP 服务端,不能写入 JavaScript、App 或公开仓库。业务系统也不能仅凭浏览器跳转结果将订单标记为已支付,必须以验签成功的异步通知或主动查单结果为准。
下面以“电脑网站支付”的密钥模式为例。手机网站支付、App 支付和当面付的整体流程相近,区别主要在调用的 API 和前端唤起方式。
接入前的准备
先在支付宝开放平台完成这些配置:
- 创建应用,取得
app_id。 - 根据业务场景开通对应产品,例如电脑网站支付或手机网站支付。
- 生成 RSA2 密钥对,将应用公钥上传到开放平台。
- 保存支付宝公钥,用它验证支付宝返回的数据。
- 配置异步通知地址
notify_url。 - 开发阶段可以使用沙箱环境,正式上线时再切换到生产网关和生产应用配置。
这几种密钥的用途不同:
- 应用私钥由商户自行保管,用于给请求签名。
- 支付宝公钥由支付宝提供,用于验证支付宝通知。
- 应用公钥需要上传给支付宝,但不能用来验证支付宝通知。
如果应用采用证书模式,还需要准备应用公钥证书、支付宝公钥证书和支付宝根证书。证书模式的请求参数和验签过程与普通公钥模式不同,应使用对应模式的官方 SDK,不能直接套用下面的密钥模式代码。
配置可以放在环境变量或服务器密钥管理系统中:
ALIPAY_APP_ID=2021000000000000
ALIPAY_PRIVATE_KEY_PATH=/secure/path/app_private_key.pem
ALIPAY_PUBLIC_KEY_PATH=/secure/path/alipay_public_key.pem
ALIPAY_NOTIFY_URL=https://pay.example.com/alipay/notify
ALIPAY_RETURN_URL=https://www.example.com/payment/result
ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
沙箱环境和生产环境使用不同的 app_id、密钥、买家账号及网关,不能混用。
发起电脑网站支付
电脑网站支付通常调用 alipay.trade.page.pay。下面的示例不依赖具体 SDK 版本,直接演示请求签名和表单提交过程。
<?php
declare(strict_types=1);
$config = [
'app_id' => getenv('ALIPAY_APP_ID'),
'private_key_path' => getenv('ALIPAY_PRIVATE_KEY_PATH'),
'notify_url' => getenv('ALIPAY_NOTIFY_URL'),
'return_url' => getenv('ALIPAY_RETURN_URL'),
'gateway' => getenv('ALIPAY_GATEWAY') ?: 'https://openapi.alipay.com/gateway.do',
];
function loadPrivateKey(string $path): OpenSSLAsymmetricKey
{
$content = file_get_contents($path);
if ($content === false) {
throw new RuntimeException('无法读取支付宝应用私钥');
}
$key = openssl_pkey_get_private($content);
if ($key === false) {
throw new RuntimeException('支付宝应用私钥格式错误');
}
return $key;
}
function buildSignContent(array $params): string
{
unset($params['sign']);
$params = array_filter(
$params,
static fn ($value) => $value !== '' && $value !== null
);
ksort($params);
$pairs = [];
foreach ($params as $key => $value) {
$pairs[] = $key . '=' . $value;
}
return implode('&', $pairs);
}
function rsa2Sign(array $params, OpenSSLAsymmetricKey $privateKey): string
{
$content = buildSignContent($params);
if (!openssl_sign($content, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
throw new RuntimeException('支付宝请求签名失败');
}
return base64_encode($signature);
}
$orderNo = 'PAY' . date('YmdHis') . random_int(100000, 999999);
/*
* 实际项目中应先把订单写入数据库。
* 金额必须从服务端商品数据计算,不能直接相信浏览器传入的金额。
*/
$bizContent = [
'out_trade_no' => $orderNo,
'total_amount' => '99.00',
'subject' => '测试订单',
'product_code' => 'FAST_INSTANT_TRADE_PAY',
];
$params = [
'app_id' => $config['app_id'],
'method' => 'alipay.trade.page.pay',
'format' => 'JSON',
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'notify_url' => $config['notify_url'],
'return_url' => $config['return_url'],
'biz_content' => json_encode(
$bizContent,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
),
];
$privateKey = loadPrivateKey($config['private_key_path']);
$params['sign'] = rsa2Sign($params, $privateKey);
?>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>正在跳转到支付宝</title>
</head>
<body>
<form id="alipay-form"
method="post"
action="<?= htmlspecialchars($config['gateway'], ENT_QUOTES, 'UTF-8') ?>">
<?php foreach ($params as $name => $value): ?>
<input type="hidden"
name="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"
value="<?= htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') ?>">
<?php endforeach; ?>
</form>
<script>
document.getElementById('alipay-form').submit();
</script>
</body>
</html>
生产环境中的订单号必须全局唯一。计算金额时建议使用字符串或最小货币单位,避免直接累加浮点数导致精度误差。
处理支付宝异步通知
用户完成支付后,支付宝会向 notify_url 发送服务器通知。通知处理至少需要检查以下内容:
- RSA2 验签是否通过。
app_id是否与当前应用一致。out_trade_no是否存在于本系统。total_amount是否与订单应付金额完全一致。- 收款方信息是否符合预期,例如
seller_id是否正确。 trade_status是否为当前业务认可的成功状态。- 重复收到同一通知时,是否会重复发货。
示例:
<?php
declare(strict_types=1);
function buildVerifyContent(array $params): string
{
unset($params['sign'], $params['sign_type']);
$params = array_filter(
$params,
static fn ($value) => $value !== '' && $value !== null
);
ksort($params);
$pairs = [];
foreach ($params as $key => $value) {
$pairs[] = $key . '=' . $value;
}
return implode('&', $pairs);
}
function verifyAlipayNotify(array $params, string $publicKeyPath): bool
{
$sign = $params['sign'] ?? '';
if ($sign === '') {
return false;
}
$publicKeyContent = file_get_contents($publicKeyPath);
if ($publicKeyContent === false) {
throw new RuntimeException('无法读取支付宝公钥');
}
$publicKey = openssl_pkey_get_public($publicKeyContent);
if ($publicKey === false) {
throw new RuntimeException('支付宝公钥格式错误');
}
$result = openssl_verify(
buildVerifyContent($params),
base64_decode($sign, true),
$publicKey,
OPENSSL_ALGO_SHA256
);
return $result === 1;
}
$params = $_POST;
try {
if (!verifyAlipayNotify(
$params,
(string) getenv('ALIPAY_PUBLIC_KEY_PATH')
)) {
http_response_code(400);
echo 'failure';
exit;
}
if (($params['app_id'] ?? '') !== getenv('ALIPAY_APP_ID')) {
http_response_code(400);
echo 'failure';
exit;
}
$orderNo = $params['out_trade_no'] ?? '';
$tradeNo = $params['trade_no'] ?? '';
$tradeStatus = $params['trade_status'] ?? '';
$paidAmount = $params['total_amount'] ?? '';
/*
* 从数据库读取订单,并核对:
* 1. 订单存在;
* 2. paidAmount 与数据库中的应付金额一致;
* 3. seller_id 等收款方信息正确;
* 4. 订单尚未被其他支付流水占用。
*/
if (in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
/*
* 在数据库事务中执行幂等更新:
* - 将订单更新为已支付;
* - 保存 tradeNo;
* - 记录原始通知;
* - 触发后续发货或充值流程。
*
* 如果订单已经处理过,应直接视为成功,不能重复发货。
*/
}
echo 'success';
} catch (Throwable $e) {
error_log($e->getMessage());
http_response_code(500);
echo 'failure';
}
只有业务处理成功后,接口才能输出纯文本 success。如果响应超时,或返回其他内容、failure,支付宝可能按规则再次发送通知,因此通知接口必须具备幂等性。
通知接口前不能设置登录验证、验证码或浏览器 Cookie 校验,同时要保证公网可以通过 HTTPS 正常访问该地址。
主动查询订单
异步通知可能因网络、网关或服务器故障而延迟。订单长时间停留在“支付中”状态时,应调用 alipay.trade.query 主动查询。
查询时通常传入商户订单号:
$bizContent = [
'out_trade_no' => $orderNo,
];
$params = [
'app_id' => $config['app_id'],
'method' => 'alipay.trade.query',
'format' => 'JSON',
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'biz_content' => json_encode(
$bizContent,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
),
];
$params['sign'] = rsa2Sign(
$params,
loadPrivateKey($config['private_key_path'])
);
将参数通过 POST 提交到支付宝网关后,还要验证网关响应签名,再根据查询结果更新订单。浏览器访问 return_url 只说明用户返回了商户页面,并不代表付款一定成功。
其他支付场景
不同支付方式的主要区别是接口名称和支付结果的交付方式:
- 电脑网站支付使用
alipay.trade.page.pay,由服务端生成跳转表单。 - 手机网站支付使用
alipay.trade.wap.pay,由服务端生成手机网页支付表单。 - App 支付使用
alipay.trade.app.pay。PHP 服务端生成签名后的订单字符串,再由客户端调用支付宝客户端 SDK。 - 当面付扫码通常由服务端调用预创建交易接口,取得二维码内容后展示给用户。
具体参数取决于已开通产品、签约状态、SDK 版本,以及采用密钥模式还是证书模式。使用支付宝官方 PHP SDK 时,应按照当前开放平台文档安装,并使用 SDK 提供的签名、验签和请求客户端。不要混用 SDK 签名与自定义签名代码。
常见问题和注意事项
- PHP 环境需要启用 OpenSSL 扩展,并支持 SHA-256。
- 私钥文件不能放在 Web 可访问目录,也不能提交到 Git。
notify_url必须是支付宝服务器能够访问的完整地址,本机localhost无法直接接收通知。- 签名前不要对参数值进行 URL 编码。提交 HTTP 请求时,再由表单或客户端编码。
- 验签必须使用支付宝公钥,不能误用应用公钥。
- 数据库存储金额时,建议使用
DECIMAL,或者统一将“分”存为整数。 - 订单状态更新、支付流水写入和库存处理应放在事务中,并通过唯一索引或状态条件保证幂等。
- 支付宝传回的
subject、金额或订单号不能直接当作可信的业务数据。 - 正式上线前需要测试支付成功、取消支付、重复通知、通知乱序、金额不一致、验签失败、查单补偿和退款等场景。
- 支付宝开放平台的产品权限、SDK 结构和证书要求可能调整,最终应以当前应用后台显示的能力及对应 API 文档为准。
备注:内容仅供参考。
