PAYATHON 2026

如何将支付页面以带 API key 认证的 API 方式开放

支付小周

结论

不要让商户服务器通过 cURL 请求支付 URL,再把返回的 HTML 直接输出到商户网站。cURL 是服务器端 HTTP 客户端,无法控制用户浏览器跳转。即使支付表单来自支付网关,只要商户页面直接输出响应内容,浏览器地址栏显示的仍是商户网站域名。

用户在一个域名下看到由另一个主体提供的银行卡信息收集页面,容易产生疑虑,也可能引起浏览器或安全检测系统的警惕。不过,仅凭这一现象无法确定 Google 作出具体判定的原因。

标准流程应分为以下几个环节:

  1. 商户服务器使用 API key 调用“创建支付会话”API。
  2. API 返回一次性的托管支付页面 URL,商户服务器让用户浏览器跳转到该 URL。
  3. 用户在支付网关自己的 HTTPS 域名下填写银行卡信息。
  4. 支付完成后,网关将用户跳转回商户页面,并通过签名 webhook 向商户服务器发送最终结果。

API key 只用于服务器之间的认证,不能出现在浏览器代码、HTML、查询字符串或公开日志中。

为什么当前代码不会改变地址栏

下面这段代码运行在商户服务器上:

$response = curl_exec($ch);

请求由服务器通过 cURL 发出,并非由用户浏览器发出。设置 CURLOPT_RETURNTRANSFER => false 后,远端返回的支付页面会直接写入当前 PHP 响应。浏览器看到的仍是商户网站返回的页面,所以地址栏不会变成支付网关的 URL。

示例中使用的地址是:

http://www.xecute/pay/

支付流程必须全程使用有效的 https:// 地址,不能依赖 HTTP 临时跳转到 HTTPS。

也不要把 cURL 响应转换成 HTML 字符串后继续输出。这样做依然是在商户域名下托管或代理银行卡输入页面,容易让用户混淆页面归属,还可能引发静态资源、Cookie、CSP、SameSite 策略及表单提交地址等问题。

推荐的托管支付流程

1. 商户服务器创建支付会话

商户后端向网关提交订单信息:

POST /v1/checkout-sessions HTTP/1.1
Host: pay.example.com
Authorization: Bearer merchant_api_key
Content-Type: application/json
Idempotency-Key: 5cc7d5fd-51ef-49ad-8ee7-cc672660191e

{
  "merchant_id": "merchant_123",
  "order_id": "order_10001",
  "amount": 12500,
  "currency": "CNY",
  "customer": {
    "email": "customer@example.com",
    "mobile": "13800000000"
  },
  "return_url": "https://merchant.example.com/payment/return"
}

金额最好用最小货币单位的整数表示。例如,12500 表示 125.00 CNY,这样可以避免浮点数精度问题。

API 验证 API key、商户状态、订单号、金额和回跳地址后,创建一个有效期较短且只能用于指定订单的支付会话:

{
  "session_id": "cs_7df218b1",
  "checkout_url": "https://pay.example.com/checkout/cs_7df218b1",
  "expires_at": "2026-09-15T15:30:00Z"
}

API 不应返回银行卡表单 HTML,也不应要求调用方通过服务器端 cURL 代理该页面。

2. 将用户浏览器重定向到支付页面

商户服务器取得 checkout_url 后,向浏览器返回重定向响应:

<?php

$ch = curl_init('https://pay.example.com/v1/checkout-sessions');

$payload = [
    'merchant_id' => 'merchant_123',
    'order_id' => 'order_10001',
    'amount' => 12500,
    'currency' => 'CNY',
    'customer' => [
        'email' => $_POST['emailID'] ?? '',
        'mobile' => $_POST['mobileNo'] ?? '',
    ],
    'return_url' => 'https://merchant.example.com/payment/return',
];

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('PAYMENT_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
    CURLOPT_TIMEOUT => 15,
]);

$responseBody = curl_exec($ch);

if ($responseBody === false) {
    throw new RuntimeException(curl_error($ch));
}

$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);

if ($statusCode !== 201 || empty($response['checkout_url'])) {
    throw new RuntimeException('Unable to create checkout session');
}

header('Location: ' . $response['checkout_url'], true, 303);
exit;

其中最重要的设置是:

CURLOPT_RETURNTRANSFER => true

它让商户服务器读取 API 返回的 JSON,而不是把响应直接输出给浏览器。取得 checkout_url 后,服务器再通过 303 See Other 让浏览器访问支付网关托管的页面。

checkout_url 必须来自可信 API,而且只能指向支付网关自己的 HTTPS 域名。不要把用户提交的任意 URL 直接放入 Location,否则可能形成开放重定向漏洞。

3. 在支付网关域名渲染表单

浏览器访问:

https://pay.example.com/checkout/cs_7df218b1

网关根据随机会话标识读取保存在服务器端的订单信息,再渲染支付表单。API key 不能放进这个 URL。会话标识需要满足以下要求:

  • 使用密码学安全的随机值,不能通过订单号简单推算。
  • 设置较短的有效期。
  • 与商户、订单、金额和币种绑定。
  • 支付成功后立即失效,防止重复使用。
  • URL 中不携带邮箱、手机号等敏感信息。
  • 对重复提交进行幂等处理。

支付页面还应明确显示支付服务方、商户名称、订单号、金额和币种,方便用户确认收款对象和支付内容。

API key 应该怎样验证

API key 用于认证调用 API 的商户服务器,不能用来认证浏览器用户。建议使用标准的 Authorization 请求头:

Authorization: Bearer sk_live_xxxxxxxxx

也可以继续使用自定义请求头:

X-API-Key: sk_live_xxxxxxxxx

无论选择哪一种方式,都需要满足以下条件:

  • 只通过 HTTPS 发送。
  • 每个商户使用独立密钥。
  • 数据库尽量保存密钥哈希,而不是可直接读取的明文。
  • 支持密钥轮换、撤销和有效期管理。
  • 区分测试密钥和生产密钥。
  • 不在访问日志或错误日志中记录完整密钥。
  • 根据密钥确定商户身份,不信任请求体中可随意填写的 merchant_id。
  • 配合速率限制、异常检测和审计日志。
  • 高安全等级场景可以进一步采用请求签名、mTLS 或 OAuth 2.0 client credentials。

只验证 merchantID 不能提供可靠的认证,因为攻击者通常可以猜到或获取公开的商户编号。

不要信任浏览器提交的金额

示例代码直接使用:

'amount' => $_POST['amount']

如果这个值来自用户浏览器,攻击者可以篡改请求,将订单金额改小。商户服务器应从自己的订单数据库中重新读取金额,再将其发送给支付 API:

$order = loadOrderFromDatabase($orderId);

$payload = [
    'order_id' => $order->id,
    'amount' => $order->amountMinor,
    'currency' => $order->currency,
];

如果同一个 order_id 使用不同的金额或币种重复创建支付会话,支付网关也应拒绝请求。

回跳页面不能作为支付成功依据

用户完成支付后,可以被重定向到:

https://merchant.example.com/payment/return?session_id=cs_7df218b1

但浏览器回跳可能中断、重复执行或被伪造。商户不能因为回跳参数中出现 status=success 就直接发货。

支付网关应向商户服务器发送 webhook:

POST /webhooks/payment HTTP/1.1
Host: merchant.example.com
Content-Type: application/json
Payment-Signature: t=1789461000,v1=...

{
  "event_id": "evt_8147",
  "type": "payment.succeeded",
  "data": {
    "session_id": "cs_7df218b1",
    "order_id": "order_10001",
    "amount": 12500,
    "currency": "CNY",
    "transaction_id": "txn_99102"
  }
}

商户验证 webhook 签名、时间戳、事件 ID、金额和币种后,才能把订单更新为已支付。网关需要支持 webhook 重试,商户端则要按 event_id 或 transaction_id 进行幂等处理。

如果 webhook 尚未到达,商户服务器也可以使用 API key 查询支付状态,但不能让浏览器自行声明支付结果。

是否可以使用 iframe

从技术上说,可以把托管支付页放入 iframe,但这种方式通常不如顶层跳转直观。用户在 iframe 中填写银行卡信息时,地址栏显示的仍是商户域名,很难确认究竟由谁处理支付信息。iframe 还可能带来内容嵌入、安全策略和第三方 Cookie 等问题。

如果业务必须使用嵌入式支付组件,应专门设计 iframe 或 hosted fields 集成方案,而不是直接嵌入完整支付页面。至少需要做到:

  • 通过 CSP frame-ancestors 等方式明确允许哪些商户域名嵌入页面。
  • 使用精确的 postMessage origin 校验来交换状态。
  • 不向父页面发送 API key 或敏感支付信息。
  • 防范点击劫持、覆盖层和恶意脚本。
  • 评估这种集成方式对 PCI DSS 范围的影响。

对通用托管支付服务来说,顶层 HTTPS 重定向通常更便于说明、实施和审计。

关于 phishing 警告

当前加载方式可能增加误判风险。用户停留在商户域名,却看到另一个支付服务提供的银行卡表单,页面归属并不清楚。不过,这不足以证明它就是 Google 标记页面的唯一原因。

还需要检查以下事项:

  • 支付域名和商户域名是否都启用了有效的 HTTPS。
  • 页面中是否存在 HTTP 混合内容。
  • 域名是否曾被入侵、托管恶意文件或发生异常跳转。
  • 页面上的品牌、公司信息、隐私政策和联系方式是否完整。
  • 是否存在隐藏 iframe、混淆脚本或来自不可信域名的资源。
  • 支付页面是否模仿银行或其他机构的登录界面。
  • 域名信誉、DNS、证书链和部署环境是否正常。
  • Google Search Console 或 Safe Browsing 是否提供了具体的安全问题说明。

确认问题已经清除后,可以通过相应的站长或安全平台申请复审。但通过复审并不代表支付架构本身已经安全。

推荐的接口边界

系统最终可以划分为三个端点:

POST /v1/checkout-sessions

由服务器调用,使用 API key 创建支付会话,并返回 checkout_url。

GET /checkout/{session_id}

由用户浏览器访问,不使用 API key,用于显示托管支付页面。

POST /webhooks/payment

由支付网关通知商户服务器,使用签名验证,不依赖浏览器回跳。

这样可以分别处理 API 认证、浏览器导航和支付结果通知,让支付页面保持在可信的网关域名下,同时避免 API key 暴露给客户端。PCI Compliant environment 只是基础条件,密钥管理、订单完整性、会话安全、日志脱敏、漏洞管理和 webhook 验证仍需分别落实。

备注:内容仅供参考。