PAYATHON 2026

支付宝支付请求重定向后被 CORS policy 阻止怎么办?

支付阿杰

结论

这不是支付宝接口请求失败,而是前端把支付页面当成了 AJAX/XHR 资源来请求。

/gateway/pay 重定向到支付宝收银台后,XMLHttpRequest 会继续访问支付宝域名,因而产生跨域请求。支付宝收银台用于浏览器页面跳转或表单提交,并不允许你的站点通过 XHR 读取页面内容,所以浏览器会根据同源策略拦截响应。

正确的处理方式如下:

  • 服务端创建支付请求并完成签名。
  • 前端通过页面跳转、表单提交或新窗口打开支付宝收银台。
  • 不要使用 axios、fetch 或 XMLHttpRequest 请求支付页面。
  • 支付结果应以服务端收到的异步通知为准,不能只依赖支付完成后的前端跳转。

为什么会触发 CORS

假设前端发起了下面的请求:

axios.get('/gateway/pay');

请求最初访问的是同源地址:

http://game.tianmei888.top/gateway/pay

随后,服务端返回重定向,将请求转向:

https://mobileclientgw-sandbox.dl.alipaydev.com/...

HTTP 客户端虽然会自动跟随重定向,但这仍然是一个 XMLHttpRequest。此时,请求目标已经从:

http://game.tianmei888.top

变成:

https://mobileclientgw-sandbox.dl.alipaydev.com

协议、域名或端口只要有一项不同,就属于跨源请求。支付宝的响应中没有:

Access-Control-Allow-Origin: http://game.tianmei888.top

因此,浏览器不允许 JavaScript 读取该响应。

这项限制由支付宝服务器的响应决定。即使在自己的网站上添加 Access-Control-Allow-Origin,也无法改变支付宝最终响应的 CORS 策略。

为什么显示 net::ERR_FAILED 200

200 表示支付宝服务器确实返回了成功的 HTTP 响应,net::ERR_FAILED 则表示浏览器拒绝将这份响应交给 JavaScript。

两者并不冲突:

  • 在 HTTP 层面,支付宝页面返回成功,状态码是 200。
  • 在浏览器安全层面,响应不符合 CORS 要求,JavaScript 无权读取。
  • 在 JavaScript 层面,这次 XHR 请求表现为失败。

失败的是对支付宝页面的跨域读取,不是支付宝收银台页面本身。

正确处理方式一:直接跳转页面

如果 /gateway/pay 会返回重定向,可以让浏览器直接访问这个地址:

window.location.href = '/gateway/pay';

也可以使用:

window.location.assign('/gateway/pay');

这样,/gateway/pay 返回的 302 重定向属于顶层页面导航,不要求目标页面提供 AJAX 所需的 Access-Control-Allow-Origin。

错误示例:

axios.get('/gateway/pay').then(response => {
  // 不要使用 AJAX 请求会跳转到支付宝收银台的接口
});

正确示例:

function goToPay() {
  window.location.href = '/gateway/pay';
}

对应的按钮可以这样写:

<button type="button" onclick="goToPay()">支付宝支付</button>

正确处理方式二:服务端返回支付宝支付表单

支付宝网页支付接口通常会生成一段自动提交的 HTML 表单。服务端可以直接将这段表单作为 HTML 响应返回,再由浏览器完成提交。

下面是 Java SDK 的示意代码。具体类名和参数应以项目实际使用的支付宝 SDK 和支付产品为准:

@GetMapping(value = "/gateway/pay", produces = "text/html;charset=UTF-8")
@ResponseBody
public String pay() throws AlipayApiException {
    AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();

    request.setReturnUrl("https://example.com/pay/return");
    request.setNotifyUrl("https://example.com/pay/notify");

    request.setBizContent("""
        {
          "out_trade_no": "ORDER_202609090001",
          "total_amount": "99.00",
          "subject": "订单支付",
          "product_code": "FAST_INSTANT_TRADE_PAY"
        }
        """);

    AlipayTradePagePayResponse response = alipayClient.pageExecute(request);

    if (!response.isSuccess()) {
        throw new IllegalStateException(
            "创建支付宝支付请求失败:" + response.getSubMsg()
        );
    }

    return response.getBody();
}

前端仍然要通过页面导航访问这个接口:

window.location.href = '/gateway/pay';

浏览器打开 /gateway/pay 后会收到 HTML 表单,然后由该表单向支付宝提交请求。整个过程不需要 XHR 读取支付宝页面。

如果支付请求必须使用 POST,可以直接提交普通表单:

<form method="post" action="/gateway/pay">
  <input type="hidden" name="orderId" value="ORDER_202609090001">
  <button type="submit">支付宝支付</button>
</form>

正确处理方式三:接口返回跳转地址,再由浏览器导航

如果当前使用的支付宝产品和 SDK 可以安全生成完整的支付地址,业务接口也可以先返回 JSON,再让前端进行页面跳转。

服务端返回:

{
  "payUrl": "https://openapi.alipaydev.com/gateway.do?..."
}

前端请求自己的业务接口,拿到地址后跳转:

const response = await fetch('/api/payment/alipay/create', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    orderId: 'ORDER_202609090001'
  })
});

if (!response.ok) {
  throw new Error('创建支付请求失败');
}

const data = await response.json();
window.location.href = data.payUrl;

这里的 AJAX 只访问同源的业务接口。访问支付宝的动作由 window.location.href 完成,所以支付宝收银台不会被当作跨域 API 响应读取。

如果 SDK 返回的是 HTML 表单,而不是 URL,应优先让服务端直接输出表单,不要假设所有支付产品都能转换成 GET 跳转地址。

支付结果应在服务端确认

return_url 只是用户支付完成后的页面跳转地址。如果用户关闭页面、网络中断或跳转失败,这个地址可能根本不会执行,因此不能单独用它判断订单是否支付成功。

订单状态应根据 notify_url 收到的支付宝异步通知来更新,并完成以下检查:

  1. 使用支付宝公钥验证通知签名。
  2. 核对 app_id、商户身份和通知中的交易信息。
  3. 确认 out_trade_no 属于当前商户。
  4. 确认 total_amount 与订单金额一致。
  5. 检查交易状态是否符合业务要求。
  6. 通过幂等逻辑更新订单,避免重复通知引发重复处理。
  7. 验证和处理成功后,按照支付宝接口要求返回确认内容。

示意逻辑如下:

@PostMapping("/pay/notify")
@ResponseBody
public String notify(HttpServletRequest request) {
    Map<String, String> params = extractAlipayParams(request);

    boolean valid = verifyAlipaySignature(params);
    if (!valid) {
        return "failure";
    }

    String orderNo = params.get("out_trade_no");
    String totalAmount = params.get("total_amount");
    String tradeStatus = params.get("trade_status");

    if (!orderMatches(orderNo, totalAmount)) {
        return "failure";
    }

    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {
        markOrderPaidIdempotently(orderNo);
        return "success";
    }

    return "failure";
}

extractAlipayParams、verifyAlipaySignature、orderMatches 和 markOrderPaidIdempotently 都是示意方法,需要根据实际使用的 SDK 和订单系统实现。

不建议使用反向代理绕过 CORS

将支付宝收银台页面代理到自己的域名,看起来可以消除跨域问题,但通常不适合支付场景:

  • 页面中的跳转、Cookie、资源地址和安全策略可能无法正常工作。
  • 代理可能破坏支付宝收银台原有的交互流程。
  • 这种做法会增加敏感参数泄露和错误缓存的风险。
  • 反向代理也无法代替服务端签名验证和异步通知处理。

CORS 并不是支付流程中需要绕过的障碍。它说明当前请求支付宝收银台的方式不对。

还需要检查的配置

改用页面跳转后,还需要检查以下配置:

  • 沙箱应用必须配合沙箱网关、沙箱账号和对应密钥使用,不能与正式环境配置混用。
  • 私钥、公钥和签名算法必须与支付宝开放平台中的应用配置一致。
  • notify_url 必须是支付宝服务器能够访问的公网地址。
  • 生产环境应使用 HTTPS,避免支付前页面和回跳页面运行在不安全的 HTTP 环境中。
  • 订单号应由服务端生成并校验,支付金额不能直接采用前端提交的值。
  • 支付接口不能向浏览器返回应用私钥、签名原文或其他敏感配置。
  • 支付按钮应防止连续点击,以免重复创建支付单。

解决这个问题不需要给支付宝响应添加 CORS header。服务端生成支付参数后,让浏览器通过页面导航或表单提交进入支付宝收银台即可,不要再用 XHR 请求收银台页面。

备注:内容仅供参考。