PAYATHON 2026

bKash payment API integration 无法访问 App URL

支付老李

结论

这通常不是 Laravel 的 CORS 问题。更可能的情况是,App URL 作为普通页面或图片可以访问,但无法被 bKash 页面通过 iframe 或 popup 流程加载。

先检查这些响应头:

  • X-Frame-Options: SAMEORIGIN 或 DENY
  • Content-Security-Policy 中的 frame-ancestors
  • 页面加载 bKash 内容时,frame-src、connect-src 和 script-src 是否允许 bKash 域名

普通链接和 <img> 能打开,只能说明 URL 基本可达,不能说明它支持跨域嵌入、脚本调用或支付重定向。CORS 主要控制 fetch、XHR 等跨源请求,不能解除浏览器对 iframe 嵌入的限制。

先确认失败发生在哪一层

需要向 bKash 确认“无法访问”具体指什么:

  1. bKash 的服务器请求 App URL 时失败;
  2. bKash 页面尝试在 iframe 中嵌入 App URL,但被浏览器阻止;
  3. App URL 可以打开,但支付 popup 中的脚本、Cookie 或 API 请求失败;
  4. URL 重定向后进入了登录页、错误页或其他域名;
  5. bKash 的自动检查程序发送了 HEAD 请求,而应用只处理 GET。

如果能拿到 bKash 返回的 HTTP 状态码、浏览器 Console 错误或失败请求的响应头,通常很快就能缩小排查范围。

检查 iframe 安全策略

如果响应中包含以下任一配置,bKash 的跨域页面可能无法嵌入 App URL:

X-Frame-Options: SAMEORIGIN
X-Frame-Options: DENY

或者:

Content-Security-Policy: frame-ancestors 'self'

浏览器控制台通常会显示类似信息:

Refused to display the page in a frame because it set
'X-Frame-Options' to 'sameorigin'.

如果 bKash 确实需要嵌入该页面,应通过 CSP 精确放行 bKash 提供的正式域名。域名必须以 bKash 的接入文档或技术人员提供的信息为准,不要根据名称猜测。

例如:

Content-Security-Policy: frame-ancestors 'self' https://<official-bkash-domain>;

不要使用这种过于宽泛的配置:

Content-Security-Policy: frame-ancestors *;

它会允许任意网站嵌入支付页面,增加点击劫持风险。

多数现代浏览器已经弃用 X-Frame-Options: ALLOW-FROM ...,不应依赖它。需要按域名放行时,应使用 CSP 的 frame-ancestors。

Laravel 中设置响应头

可以只为 bKash 支付入口配置中间件,以免放宽整个应用的安全策略。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class BkashFramePolicy
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // 这里必须替换为 bKash 明确提供的实际域名。
        $response->headers->set(
            'Content-Security-Policy',
            "frame-ancestors 'self' https://<official-bkash-domain>"
        );

        // 避免上游中间件继续发送 SAMEORIGIN 或 DENY。
        $response->headers->remove('X-Frame-Options');

        return $response;
    }
}

然后将中间件应用到有限的支付路由:

Route::get('/payment/bkash', [BkashController::class, 'show'])
    ->middleware('bkash.frame');

具体如何注册取决于 Laravel 版本和项目结构。没有确认版本时,不要直接照搬某个固定注册文件的写法。

还要检查 Nginx、Apache、CDN、负载均衡器和安全插件。即使 Laravel 删除了响应头,上游服务器仍可能重新添加:

add_header X-Frame-Options SAMEORIGIN;

如果存在这项配置,应针对支付路径调整,不能只修改 Laravel。

检查页面加载 bKash 资源的 CSP

如果是你的页面负责加载 bKash popup,问题也可能不在 frame-ancestors,而是 CSP 阻止了页面连接 bKash。

例如,下面的策略可能过于严格:

Content-Security-Policy: default-src 'self'; frame-src 'self'; connect-src 'self';

此时需要根据实际集成方式,放行 bKash 官方提供的资源域名:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://<official-bkash-script-domain>;
  connect-src 'self' https://<official-bkash-api-domain>;
  frame-src 'self' https://<official-bkash-frame-domain>;
  frame-ancestors 'self' https://<official-bkash-parent-domain>;

这些占位域名不能直接用于生产环境。sandbox 和 production 域名需要分别核对,因为测试环境可以正常工作,并不代表生产审核系统使用相同的域名。

从外部验证 HTTP 响应

不要只在已经登录或连接公司网络的浏览器中测试。可以从外部网络检查:

curl -I https://example.com/payment/bkash

进一步查看重定向过程和最终响应:

curl -L -v https://example.com/payment/bkash

模拟跨站来源:

curl -I \
  -H 'Origin: https://<official-bkash-domain>' \
  -H 'Referer: https://<official-bkash-domain>/' \
  https://example.com/payment/bkash

重点检查:

  • 是否返回 200;
  • 是否先返回 301、302、401、403、419 或 500;
  • 最终 URL 是否仍为 HTTPS;
  • 是否被重定向到登录页面;
  • 是否存在 X-Frame-Options;
  • CSP 是否阻止 bKash;
  • 更换请求方法或 User-Agent 后,响应是否发生变化。

还需要测试 HEAD:

curl -I -X HEAD https://example.com/payment/bkash

有些自动审核系统会先发送 HEAD 请求。如果返回 405 Method Not Allowed,可以让服务器正确响应 HEAD,或者请 bKash 确认其验证方式。

排查网络、DNS 和 TLS

如果 bKash 的服务器根本无法建立连接,常见原因包括以下几类。

DNS 配置异常

检查:

  • 公网 DNS 是否已经生效;
  • 是否错误配置了 AAAA 记录;
  • 是否存在 IPv4 可用但 IPv6 不可用的情况;
  • bKash 所在网络是否解析到了旧地址或内网地址。

一种常见情况是,域名同时配置了 A 和 AAAA 记录,但服务器没有正确提供 IPv6 服务。部分客户端会优先使用 IPv6,结果就会出现“自己能访问,第三方不能访问”。

TLS 证书链不完整

浏览器可能会缓存或补全中间证书,但服务器端程序一般会严格校验证书链。需要检查:

  • 证书是否过期;
  • 域名是否与证书匹配;
  • 中间证书链是否完整;
  • 是否只支持过旧的 TLS 或加密套件;
  • 是否存在 SNI 配置错误。

防火墙或 WAF 拦截

Cloudflare、服务器防火墙或主机商的安全策略可能根据以下条件拒绝请求:

  • 来源 IP;
  • 国家或地区;
  • User-Agent;
  • 请求频率;
  • 请求是否携带 Cookie;
  • 是否来自数据中心网络或自动化客户端;
  • 是否为 HEAD 请求;
  • 是否触发 Bot 防护或 JavaScript Challenge。

查看 Web 服务器、CDN 和 WAF 日志,确认其中是否有 bKash 的请求。如果请求已经到达并返回 403,应让 bKash 提供出口 IP,然后采用范围尽可能小的放行策略。如果日志里完全没有请求,则继续检查 DNS、TLS 和上游网络。

检查认证、Cookie 和 CSRF

App URL 应该能在新的无痕会话中直接访问,不能依赖以下条件:

  • 用户已经登录;
  • 浏览器中已经保存 session;
  • 特定 Cookie 已经存在;
  • 请求来自站内页面;
  • 本地缓存中已经有 CSRF token。

如果页面被跨站嵌入,Cookie 还会受到 SameSite 限制。如果确实需要在跨站 iframe 中发送 session Cookie,通常需要:

SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=none

对应的 Cookie 必须同时包含:

SameSite=None; Secure

即使这样,第三方 Cookie 仍可能被浏览器或隐私策略拦截。因此,支付流程不应把跨站 iframe Cookie 作为唯一的状态依据。更可靠的方式是使用短期、一次性的服务端支付状态标识,并在回调时重新校验交易。

对于 bKash 发起的服务端回调,不要简单关闭整个站点的 CSRF。应当:

  • 只为指定回调路由配置有限例外;
  • 校验 bKash 要求的签名、token 或交易标识;
  • 校验订单金额、币种和订单状态;
  • 保证回调处理具有幂等性。

检查 URL 和重定向

App URL 最好满足以下条件:

  • 使用标准的公网 HTTPS 地址;
  • 不需要 Basic Authentication;
  • 不包含仅在本地有效的域名;
  • 不重定向到 localhost、内网 IP 或其他端口;
  • 不出现 HTTP 与 HTTPS 循环跳转;
  • 不依赖浏览器自动补全路径;
  • URL 大小写处理一致;
  • 可以在新的无痕窗口中直接打开。

如果 Laravel 位于反向代理之后,还要确认受信任代理配置正确。否则,应用可能把 HTTPS 请求识别成 HTTP,进而生成错误的回调地址或导致循环重定向。

建议的排查顺序

  1. 使用浏览器无痕模式直接访问 App URL,确认页面不依赖登录状态和已有 Cookie。
  2. 在开发者工具的 Network 和 Console 中查看 iframe、CSP、Cookie 以及重定向错误。
  3. 检查最终响应中的 X-Frame-Options 和 Content-Security-Policy。
  4. 检查 Nginx、Apache、CDN 和 WAF 是否覆盖了 Laravel 设置的响应头。
  5. 验证 GET 和 HEAD 是否都能得到合理响应。
  6. 检查公网 DNS、IPv6、TLS 证书链和非标准端口。
  7. 查看服务器访问日志,确认 bKash 请求是否到达,以及服务器返回了什么状态码。
  8. 向 bKash 索取失败时间、来源 IP、目标 URL、HTTP 状态码和正式环境域名。
  9. 只为支付入口放行必要的 bKash 域名,然后重新测试。

缺少实际报错截图、HTTP 状态码和响应头时,还无法确定唯一原因。但如果“链接能打开,放进 <img> 也能访问,第三方页面却无法加载”,应该先检查 X-Frame-Options、CSP 和跨站 Cookie,而不是继续扩大 CORS 权限。

备注:内容仅供参考。