Content Security Policy 导致支付回调页面 404
结论
这通常不是 Angular 路由被 Content Security Policy(CSP)“重定向成了 404”,而是支付网关通过 POST 表单返回数据,但 Angular SSR 服务端只处理页面的 GET 请求,没有对应的 POST 回调接口。请求到达服务器后找不到匹配路由,于是返回 404。
建议按以下顺序处理:
- 在浏览器 Network 面板中确认回调请求的 URL、HTTP 方法和响应状态。
- 在服务端创建专用的支付回调接口,用来接收并验证支付网关提交的数据。
- 验签成功后,通过
303 See Other跳转到 Angular 的GET结果页。 - 如果控制台明确报告某条 CSP 指令被违反,再针对该指令调整策略,不要直接放宽全部 CSP 配置。
先区分 404 和 CSP 拦截
404 和 CSP 拦截是两类问题:
404 Not Found:请求已经到达服务器,但服务器没有匹配的路由,或者 SSR、反向代理没有正确处理该请求。- CSP 拦截:浏览器在发起请求、提交表单、加载 iframe 或执行脚本前阻止了操作。控制台通常会显示类似
Refused to ... because it violates the following Content Security Policy directive的错误。
支付网关经常通过自动提交表单返回结果:
<form method="POST" action="https://shop.example.com/payment/callback">
<input type="hidden" name="transactionId" value="...">
<input type="hidden" name="signature" value="...">
</form>
假设服务器只有下面这个 Angular 页面路由:
/payment/result
却没有接收 POST /payment/callback 的服务端接口,那么请求无法进入 Angular 客户端路由,SSR 服务器或反向代理很可能直接返回 404。
排查步骤
1. 检查实际回调请求
在浏览器开发者工具的 Network 面板中找到失败的请求,重点检查:
- Request URL 是否与支付网关后台配置完全一致
- Request Method 是
GET还是POST - Status Code 来自支付网关还是本站服务器
- 请求是否经过
301、302、303、307或308跳转 - Response Headers 中是否包含
Content-Security-Policy - 表单数据使用了什么
Content-Type
支付网关常用的提交类型是:
Content-Type: application/x-www-form-urlencoded
也可能是:
Content-Type: multipart/form-data
不要只看最终显示的 404 页面,还要检查完整的请求和跳转链。
2. 查看浏览器控制台中的完整 CSP 错误
需要确认具体违反了哪条指令。例如:
Refused to send form data to 'https://...' because it violates
the following Content Security Policy directive: "form-action 'self'"
或者:
Refused to frame 'https://...' because it violates
the following Content Security Policy directive: "frame-src 'self'"
不同指令控制的内容不同:
| CSP 指令 | 影响范围 |
|---|---|
form-action | 限制当前页面可以向哪些地址提交表单 |
frame-src | 限制当前页面可以加载哪些 iframe |
frame-ancestors | 限制当前页面可以被哪些站点嵌入 |
connect-src | 限制 fetch、XHR、WebSocket 等连接 |
script-src | 限制脚本来源和内联脚本执行 |
default-src | 未单独配置其他指令时使用的默认来源 |
普通的顶层页面跳转通常不受 connect-src 控制。看到”重定向”并不意味着应该修改 connect-src。
3. 直接测试回调地址
分别测试回调地址处理 GET 和 POST 的结果:
curl -i https://shop.example.com/payment/callback
curl -i -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'transactionId=test&status=success&signature=test' \
https://shop.example.com/payment/callback
如果 GET 可以返回页面,而 POST 返回 404,基本可以确定服务端缺少 POST 路由。这里的测试数据不能证明真实支付是否成功,正式接口仍须验证签名。
推荐的服务端处理方式
支付回调不应直接交给 Angular 页面处理。可以采用下面的流程:
支付网关
↓ POST 表单
/payment/callback(服务端接收并验签)
↓ 303 Redirect
/payment/result?reference=...(Angular SSR/客户端结果页)
以常见的 Angular Universal + Express 结构为例,可以在 SSR 通配路由之前注册回调接口:
import express, { Request, Response } from 'express';
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.post('/payment/callback', async (req: Request, res: Response) => {
const payload = req.body;
try {
const isValid = verifyPaymentSignature(payload);
if (!isValid) {
return res.status(400).send('Invalid payment signature');
}
// 应根据 transactionId 查询本地订单,
// 并按支付网关协议校验金额、币种、商户号和支付状态。
const order = await updateOrderFromGateway(payload);
// POST 完成后跳转到 GET 页面,避免刷新时重复提交表单。
return res.redirect(
303,
`/payment/result?reference=${encodeURIComponent(order.publicReference)}`
);
} catch (error) {
console.error('Payment callback failed', error);
return res.redirect(303, '/payment/result?status=processing');
}
});
// 静态资源和 Angular SSR 通配路由应放在回调接口之后。
app.get('*', (req: Request, res: Response) => {
// 调用 Angular Universal 的 SSR 渲染逻辑。
});
示例中的 verifyPaymentSignature() 和 updateOrderFromGateway() 必须按照实际支付网关的协议实现。不能仅凭请求中的 status=success 判断支付成功。
303 会明确要求浏览器在跳转后发起 GET 请求。如果使用 307 或 308,浏览器会保留原来的 POST 方法,结果页可能再次返回 404。
检查反向代理配置
如果应用前面还有 Nginx、CDN、负载均衡器或托管平台,需要确认这些组件没有把 POST 请求当作静态页面请求处理。
例如,可以把回调请求转发给 Node.js 服务:
location = /payment/callback {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
}
如果使用静态托管,即使 Angular 配置了客户端路由回退,通常也无法接收支付网关提交的 POST 表单。此时需要增加后端、Serverless Function,或使用支付平台支持的服务端回调接口。
CSP 应该如何调整
只有控制台明确显示本站 CSP 阻止了操作时,才需要修改策略。
如果本站页面要向支付网关提交表单,可以放行指定的支付域名:
Content-Security-Policy: default-src 'self'; form-action 'self' https://payments.example-gateway.com;
如果支付网关通过 iframe 嵌入,可能还需要配置:
Content-Security-Policy: default-src 'self'; frame-src 'self' https://payments.example-gateway.com;
如果支付结果页需要嵌入支付网关页面,则要检查结果页的 frame-ancestors:
Content-Security-Policy: default-src 'self'; frame-ancestors 'self' https://payments.example-gateway.com;
其中的域名必须替换成支付服务商文档明确列出的生产环境域名。不要使用这种过度宽松的配置:
Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval';
这会明显削弱 CSP 对 XSS 和恶意资源加载的防护。
还要注意,form-action 检查的是发起表单提交页面的 CSP。如果表单完全由支付网关页面生成并提交,而控制台显示是支付网关自己的 CSP 阻止了提交,那么修改本站响应头无法解决问题。此时需要支付服务商修正配置,或确认回调地址是否符合其要求。
Angular 路由配置
服务端处理完回调后,Angular 中仍需配置对应的结果页路由:
const routes: Routes = [
{
path: 'payment/result',
component: PaymentResultComponent
},
{
path: '**',
component: NotFoundComponent
}
];
结果页最好只接收公开的订单引用,再向本站后端查询最终状态:
this.route.queryParamMap.subscribe(params => {
const reference = params.get('reference');
if (reference) {
this.paymentService.getPaymentStatus(reference).subscribe(status => {
this.paymentStatus = status;
});
}
});
不要把金额、支付成功状态或敏感支付数据直接放入查询参数,并据此更新订单,因为用户可以修改这些参数。
还需要注意的事项
- 浏览器返回页和支付网关的服务器通知通常是两个不同的通道。订单最终状态应以经过验签的服务端通知或主动查询结果为准。
- 回调接口应支持幂等处理。同一笔交易通知可能被重复发送,不能因此重复扣库存、发货或记账。
- 必须校验签名、商户号、订单号、金额、币种和交易状态。
- 回调 URL 的协议、域名、路径、端口和末尾斜杠必须与支付网关配置完全一致。
- 检查 HTTP 到 HTTPS 的跳转。部分跳转状态码会改变请求方法或丢失表单数据。
- CSP 可能由应用、Nginx、CDN 和安全中间件同时设置。多个
Content-Security-Policy响应头会一起生效,最终策略可能比预期更严格。 - 不要只修改 Angular 中的
<meta http-equiv="Content-Security-Policy">。如果服务器响应头也设置了 CSP,两处都需要检查,而且部分指令不适合通过meta配置。 - 不要在日志中完整记录银行卡信息、令牌、签名密钥或其他敏感支付数据。
如果失败的请求确实是 POST,应先补上服务端回调路由,并采用”接收、验签、落库、303 跳转”的流程。如果请求根本没有发出,再根据控制台报告的具体 CSP 指令,只放行必要的域名。
备注:内容仅供参考。
