PAYATHON 2026

Payfort 客户端集成第二步出现 CORS Error

支付小周

结论

这类 CORS Error 通常与请求参数无关。问题在于浏览器不允许前端 JavaScript 直接调用 PayFort 的支付接口。

PayFort(现 Amazon Payment Services)的部分流程可以在客户端完成,比如采集银行卡信息并获取 token_name。支付执行、签名生成及部分 3-D Secure 流程通常要由商户后端发起。不要在浏览器中使用 axios 直接调用支付接口,也不要把签名密钥放到前端。

为什么 ASP.NET 可以,浏览器却报错

ASP.NET 发送的是服务器到服务器的 HTTP 请求,不受浏览器同源策略限制。浏览器中的 axios 发送跨域请求时,目标服务器必须返回类似下面的响应头:

Access-Control-Allow-Origin: https://your-domain.example

你的请求还指定了:

Content-Type: application/json

这通常会触发 CORS 预检请求:

OPTIONS /payment-endpoint

如果 PayFort 接口不处理这个 OPTIONS 请求,或者没有返回允许跨域的响应头,浏览器会在真正发送 POST 之前将请求拦截。修改 Axios 配置解决不了这个限制,也不应使用 mode: "no-cors",因为代码仍然无法正常读取支付响应。

推荐的集成结构

支付流程应由客户端和后端分别处理:

  1. 客户端通过 PayFort 提供的令牌化流程获取 token_name。
  2. 客户端只将 token_name、订单编号等必要信息提交给自己的后端。
  3. 后端验证订单金额和用户身份。
  4. 后端生成 signature,再向 PayFort 发起支付请求。
  5. 如果响应要求进行 3-D Secure 验证,让浏览器跳转或提交表单到对应地址。
  6. 支付完成后,由后端校验返回参数、签名和最终交易状态。

数据流大致如下:

Browser
  ├─ 获取 token_name
  └─ POST /api/payments
          ↓
Merchant Backend
  ├─ 从数据库读取订单金额
  ├─ 生成 signature
  └─ POST PayFort API
          ↓
PayFort / 3-D Secure

前端代码示例

前端不再直接请求 payFortUrl2,而是调用自己的后端:

_getAPIData = (paymentData) => ({
  token_name: parseQueryString("token_name"),
  merchant_reference: paymentData.merchantReference,
  remember_me: paymentData.cmsData.rememberMe,
  language: paymentData.language
});

_get3dUrl = async (paymentData) => {
  const response = await axios({
    method: "POST",
    url: "/api/payments",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json"
    },
    data: this._getAPIData(paymentData)
  });

  const result = response.data;

  if (result.redirectUrl) {
    window.location.assign(result.redirectUrl);
    return;
  }

  return result;
};

前端提交的内容应尽量精简。如果后端可以根据登录用户和订单记录确定 amount、currency、customer_email 等数据,就不应直接信任客户端传入的值。

后端请求示例

下面用 Node.js/Express 展示处理流程。签名算法、字段顺序、请求格式和响应字段,应以你当前使用的 PayFort 接口文档为准:

import express from "express";
import axios from "axios";

const app = express();

app.use(express.json());

app.post("/api/payments", async (req, res) => {
  const {
    token_name,
    merchant_reference,
    remember_me,
    language
  } = req.body;

  // 必须从可信数据源读取订单,不要采用客户端提交的金额。
  const order = await loadOrder(merchant_reference);

  if (!order || order.status !== "pending") {
    return res.status(400).json({
      error: "Invalid order"
    });
  }

  const apiData = {
    command: "PURCHASE",
    access_code: process.env.PAYFORT_ACCESS_CODE,
    merchant_identifier: process.env.PAYFORT_MERCHANT_IDENTIFIER,
    merchant_reference: order.reference,
    amount: order.amountInMinorUnits,
    currency: order.currency,
    customer_email: order.customerEmail,
    customer_ip: getClientIp(req),
    token_name,
    remember_me,
    language,
    return_url: "https://merchant.example/payment/return"
  };

  apiData.signature = createPayFortSignature(
    apiData,
    process.env.PAYFORT_REQUEST_PHRASE
  );

  try {
    const payfortResponse = await axios.post(
      process.env.PAYFORT_PAYMENT_URL,
      apiData,
      {
        headers: {
          "Content-Type": "application/json",
          "Accept": "application/json"
        }
      }
    );

    const data = payfortResponse.data;

    res.json({
      status: data.status,
      responseCode: data.response_code,
      responseMessage: data.response_message,
      redirectUrl: data["3ds_url"] ?? null
    });
  } catch (error) {
    res.status(502).json({
      error: "Payment provider request failed"
    });
  }
});

createPayFortSignature() 只能存在于后端。不要将 request phrase、SHA phrase、API 密钥或其他商户凭证发送给浏览器。

3-D Secure 可能需要表单跳转

部分支付流程返回的不是可供 Axios 继续调用的 JSON API,而是要求浏览器跳转到发卡行的验证页面。这时应根据接口响应和对应的集成文档执行重定向,或者动态创建并提交 HTML 表单:

function submitRedirectForm(url, fields) {
  const form = document.createElement("form");
  form.method = "POST";
  form.action = url;

  Object.entries(fields).forEach(([name, value]) => {
    const input = document.createElement("input");
    input.type = "hidden";
    input.name = name;
    input.value = String(value);
    form.appendChild(input);
  });

  document.body.appendChild(form);
  form.submit();
}

普通页面跳转或表单提交属于浏览器导航,与通过 Axios 读取跨域响应不同,因此不会以同样的方式受到 CORS 限制。

需要检查的事项

  • 确认 payFortUrl2 是令牌化接口、支付接口,还是 3-D Secure 跳转地址。
  • 根据所用接口确认请求格式。某些端点可能要求使用 HTML form 或 application/x-www-form-urlencoded,未必接受 JSON。
  • amount 通常要用最小货币单位表示,具体换算规则以币种和接口文档为准。
  • customer_ip 应由后端从可信代理头或连接信息中获取,不能直接相信前端提交的值。
  • 在后端校验 PayFort 返回的 signature,不要只根据前端跳转页面判断支付是否成功。
  • 最终订单状态应以后端查询、回调通知或经过签名验证的支付响应为准。
  • 不要通过浏览器扩展、公共 CORS 代理或关闭浏览器安全策略来绕过错误。这些方法不适用于生产支付环境。

当前代码最需要调整的是保留客户端令牌化流程,将第二步的 axios(payfortConfig) 移到商户后端,由后端完成签名、支付请求和响应验证。

备注:内容仅供参考。