Edge 中 3D Secure Payment 返回 405 Method Not Allowed
结论
这通常不是 Angular 本身的问题。405 Method Not Allowed 表示请求已经到达服务器,但目标 URL 不接受 Payment Gateway 使用的 HTTP 方法。
3D Secure 完成后,网关常会自动提交表单,通过 POST 将 PaRes、MD、cres 或 threeDSSessionData 等结果发送到回调地址。如果回调地址直接指向 Angular 页面、静态文件服务器,或者只允许 GET 的 API 路由,服务器就会返回 405。
现代 Edge 和 Chrome 都基于 Chromium,正常情况下不会无故将有效请求变成 405。如果问题只出现在 Edge,应先比较浏览器实际发出的请求,不要急着修改 Angular 代码。
首先检查实际请求
在 Edge 中打开开发者工具的 Network 面板,重新完成一次支付,找到失败的请求并记录:
- Request URL
- Request Method
- Status Code
- Response Headers 中的
Allow、Server和Location - Request Payload 或 Form Data
- 是否发生过
301、302、303、307、308重定向 - 405 是由应用服务器、IIS、Nginx、API Gateway、WAF 还是 CDN 返回的
再与正常浏览器的请求逐项比较,重点查看以下差异:
- Edge 是否向 UI 地址发送了
POST,而其他浏览器最终发送的是GET。 - Edge 是否进入了不同的域名、兼容模式、代理或企业安全策略。
- 重定向过程中是否保留了原始 HTTP 方法。
- 回调所需的 Cookie 是否缺失,导致服务器进入错误路由。
- 网关是否根据 User-Agent 返回了不同的 3D Secure 流程。
响应头中的 Allow 很有参考价值。例如:
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
这说明目标资源只接受 GET 或 HEAD,而网关很可能提交了 POST。
推荐的回调流程
不要让 Payment Gateway 直接 POST 到 Angular 路由。更稳妥的流程如下:
Payment Gateway
POST /api/payment/3ds/callback
↓
后端验证支付结果并更新订单
↓
303 See Other
↓
GET /payment/result?orderId=...
↓
Angular 展示结果
Angular 是运行在浏览器中的客户端程序,无法在服务器上增加 POST 路由。如果静态服务器不接受 POST /payment/result,Angular 应用甚至没有机会启动。
后端处理示例
下面的 Express 示例用于接收网关的表单回调。验证完成后,服务器使用 303 跳转到 Angular 页面:
import express from "express";
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.post("/api/payment/3ds/callback", async (req, res) => {
const { PaRes, MD, cres, threeDSSessionData } = req.body;
// 根据网关协议验证签名、交易状态和订单关联关系。
// 不要仅根据浏览器提交的字段判断支付成功。
await verifyAndUpdatePayment({
PaRes,
MD,
cres,
threeDSSessionData
});
// 303 明确要求后续导航使用 GET。
res.redirect(303, "/payment/result");
});
app.get("/payment/status/:orderId", async (req, res) => {
const payment = await findPayment(req.params.orderId);
res.json(payment);
});
这里使用 303 See Other 比依赖 302 在不同浏览器中的行为更合适,因为它明确要求后续页面通过 GET 获取。
如果后端使用 ASP.NET Core,可以采用类似的处理方式:
[ApiController]
public class PaymentController : ControllerBase
{
[HttpPost("/api/payment/3ds/callback")]
public async Task<IActionResult> Callback([FromForm] ThreeDsCallback request)
{
await VerifyAndUpdatePaymentAsync(request);
return Redirect("/payment/result");
}
}
如需显式返回 303:
Response.StatusCode = StatusCodes.Status303SeeOther;
Response.Headers.Location = "/payment/result";
return new EmptyResult();
具体字段名和验证步骤必须以接入网关的协议为准。3DS1 常见的字段是 PaRes、MD,3DS2 常见的字段是 cres、threeDSSessionData,不能假定所有网关都使用相同字段。
Angular 端应该做什么
Angular 页面只负责通过 GET 加载,然后从后端查询最终状态:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-payment-result',
template: `
<p *ngIf="loading">正在确认支付结果...</p>
<p *ngIf="!loading">{{ message }}</p>
`
})
export class PaymentResultComponent implements OnInit {
loading = true;
message = '';
constructor(
private route: ActivatedRoute,
private http: HttpClient
) {}
ngOnInit(): void {
const orderId = this.route.snapshot.queryParamMap.get('orderId');
if (!orderId) {
this.loading = false;
this.message = '缺少订单信息';
return;
}
this.http
.get<{ status: string }>(`/api/payment/status/${encodeURIComponent(orderId)}`)
.subscribe({
next: result => {
this.loading = false;
this.message = `支付状态:${result.status}`;
},
error: () => {
this.loading = false;
this.message = '暂时无法查询支付状态';
}
});
}
}
不要将 status=success 之类的浏览器查询参数视为可信的支付结果。展示结果前,页面应从后端查询已经验证并持久化的交易状态。
检查重定向状态码
重定向状态码会影响后续请求所用的方法:
303 See Other:后续请求使用GET,适合在 POST 回调完成后跳转到结果页。307 Temporary Redirect:保留原请求方法和请求体。308 Permanent Redirect:同样保留原请求方法和请求体。301、302:不同历史客户端的行为可能有差异,不应依赖它们隐式转换请求方法。
例如,网关先执行 POST /callback,服务器再返回 307 并指向 /payment/result,浏览器将继续执行:
POST /payment/result
如果 /payment/result 是只允许 GET 的 Angular 静态路由,服务器就会返回 405。此时应该将回调后的重定向改为 303,而不是修改 Angular Router。
检查 IIS、Nginx 和 API 路由配置
如果应用部署在 IIS 上,应确认请求没有被 WebDAV、Request Filtering 或错误的 Handler Mapping 拦截。如果 405 在请求进入应用代码之前就已返回,需要修改 IIS 配置或路由映射。
Nginx 也可能显式限制请求方法,例如:
location / {
limit_except GET HEAD {
deny all;
}
try_files $uri $uri/ /index.html;
}
这条规则适用于静态前端,但不适用于网关的 POST 回调。API 回调应该单独配置,并代理到后端:
location /api/payment/3ds/callback {
proxy_pass http://payment_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
还要检查后端路由是否只声明了 GET:
// 错误:网关发送 POST 时会返回 405
app.get("/api/payment/3ds/callback", callbackHandler);
// 正确:方法必须与网关协议一致
app.post("/api/payment/3ds/callback", callbackHandler);
Edge 专属问题的进一步排查
如果已经确认服务器配置正确,但问题仍然只在 Edge 出现,可以继续检查:
- 是否启用了 Edge 的 IE 模式或企业兼容性列表。
- 是否安装了拦截支付页面、Cookie 或重定向的扩展。
- 是否存在企业代理、终端安全软件或 WAF 规则。
- 在 InPrivate 模式下是否仍能复现。
- Edge 的 Tracking Prevention 是否影响了第三方 Cookie。
- 回调 Cookie 是否设置了正确的
SameSite和Secure属性。 - Edge 和正常浏览器是否使用相同的环境、域名及网关会话。
跨站 3D Secure 流程可能需要 Cookie 来关联状态。是否应使用以下配置,要根据实际流程确定:
Set-Cookie: payment_session=...; Path=/; Secure; HttpOnly; SameSite=None
SameSite=None 必须与 Secure 一起使用。不过,Cookie 缺失通常会导致会话或状态校验失败,不能据此认定它就是 405 的原因。判断时仍应以 Network 面板和服务器日志为准。
CORS 是否相关
如果 Payment Gateway 通过浏览器顶层导航或 HTML 表单提交回调,这通常不是 Angular 发起的 AJAX 请求,修改 Angular 的 CORS 设置一般解决不了问题。
只有页面脚本通过 fetch 或 XMLHttpRequest 跨域调用 API 时,才需要重点检查预检请求:
OPTIONS /api/payment/3ds/callback
如果失败的是 OPTIONS,服务器需要允许预检请求和实际请求方法。如果失败的是网关表单提交的 POST,则应重点检查后端路由、服务器限制和重定向状态码。
安全注意事项
3D Secure 回调涉及支付结果,处理时还需要做到以下几点:
- 按网关要求验证签名、JWT、加密结果或服务端查询结果。
- 校验订单号、金额、币种和商户标识。
- 防止重复回调引发重复扣款或重复入账。
- 不要在 URL 中传递完整支付凭证、
PaRes、cres或敏感数据。 - 浏览器跳转结果只用于展示,支付是否成功应以服务器验证或网关通知为准。
- 如果回调接口需要排除 CSRF 检查,只能对精确路径放行,同时保留网关签名验证,不能关闭整个站点的 CSRF 防护。
应优先采用这样的流程:网关将 POST 发送到后端回调,后端完成验证后,再通过 303 跳转到 Angular 的 GET 页面。只有抓包确认 Edge 发出的请求确实与其他浏览器不同,才需要继续排查 Edge、Cookie、兼容模式或企业策略。
备注:内容仅供参考。
