如何在 AngularJS 页面接收 POST request?
结论
AngularJS 的 $routeProvider 只处理浏览器加载应用后的客户端路由,无法接收 Payment Gateway 发往服务器的 HTTP POST 请求。
应按下面的方式处理:
- 在后端创建真实的 Callback 接口,接收支付平台发来的
POST。 - 验证回调签名,并根据订单号向支付平台核验支付状态。
- 保存支付结果。
- 如果请求是浏览器的前台回跳,后端返回
303 See Other,把浏览器重定向到 AngularJS 的GET页面。 - AngularJS 成功页通过后端 API 查询订单的最终状态。
支付回调和 AngularJS 页面路由需要分开处理。
为什么 $routeProvider 没有生效
以下配置注册的是 AngularJS 客户端路由:
$routeProvider.when('/booking-success', {
templateUrl: 'views/booking-success.html',
controller: 'BookingSuccessController'
});
它只有在以下条件都满足时才会运行:
- 浏览器已经通过
GET加载 AngularJS 应用; - AngularJS 已经启动;
- 客户端路由检测到当前路径为
/booking-success。
Payment Gateway 发送的请求则是:
POST /booking-success HTTP/1.1
Content-Type: application/x-www-form-urlencoded
这个请求会先到达 Web 服务器或后端应用。此时 AngularJS 还没有运行,服务器必须决定如何处理这个 POST。如果服务器没有对应的 POST 路由,通常会返回 404、405 Method Not Allowed,或者直接拒绝请求。
在浏览器地址栏中直接打开该地址可以成功,因为地址栏发出的是 GET /booking-success。它和支付平台发送的 POST /booking-success 是两种不同的请求。
推荐的请求流程
可以为支付通知和用户回跳分别设置地址:
支付平台服务端
POST /api/payments/callback
|
v
验签、核验并更新订单
用户浏览器
GET /booking-success?orderId=...
|
v
AngularJS 成功页面
|
v
GET /api/orders/:orderId/status
如果支付平台只能通过表单 POST 把用户带回网站,可以使用以下流程:
POST /payment/return
|
v
后端处理并返回 303
|
v
GET /booking-success?orderId=...
不要让 AngularJS 直接处理 POST 回调。
后端接收回调并重定向
下面以 Express 为例。实际使用的字段名、签名算法和响应内容,必须以对应 Payment Gateway 的文档为准。
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.post('/payment/return', async (req, res) => {
const paymentData = req.body;
try {
const signatureValid = verifyGatewaySignature(paymentData);
if (!signatureValid) {
return res.status(400).send('Invalid signature');
}
// 不应只相信回调中的 success 字段。
// 在支付平台支持的情况下,应通过其服务端 API 核验交易状态。
const payment = await queryPaymentFromGateway(
paymentData.transactionId
);
await updateOrderPaymentStatus({
orderId: paymentData.orderId,
transactionId: payment.transactionId,
status: payment.status
});
const orderId = encodeURIComponent(paymentData.orderId);
// POST/Redirect/GET:明确要求浏览器下一步使用 GET。
return res.redirect(303, `/booking-success?orderId=${orderId}`);
} catch (error) {
console.error('Payment return processing failed:', error);
return res.redirect(303, '/booking-failed');
}
});
这里使用 303 See Other,是为了让浏览器在重定向后发起新的 GET 请求,避免再次提交原来的 POST。
不建议依赖默认的 302 行为来处理支付回跳,因为不同客户端处理 POST 重定向的方式可能不同。307 和 308 会保留原请求方法,也不适合把 POST 转成成功页的 GET。
AngularJS 路由配置
如果应用使用 HTML5 路由模式,可以这样配置:
angular.module('bookingApp', ['ngRoute'])
.config(function ($routeProvider, $locationProvider) {
$routeProvider
.when('/booking', {
templateUrl: 'views/booking.html',
controller: 'BookingController'
})
.when('/booking-success', {
templateUrl: 'views/booking-success.html',
controller: 'BookingSuccessController'
})
.when('/booking-failed', {
templateUrl: 'views/booking-failed.html'
})
.otherwise({
redirectTo: '/booking'
});
$locationProvider.html5Mode(true);
});
成功页根据订单号向后端查询最终结果:
angular.module('bookingApp')
.controller('BookingSuccessController', function (
$scope,
$http,
$location
) {
const orderId = $location.search().orderId;
$scope.loading = true;
$http.get(
'/api/orders/' + encodeURIComponent(orderId) + '/payment-status'
).then(function (response) {
$scope.payment = response.data;
}).catch(function () {
$scope.error = '暂时无法获取支付结果,请稍后刷新页面。';
}).finally(function () {
$scope.loading = false;
});
});
后端状态接口只需返回页面展示所需的信息:
app.get('/api/orders/:orderId/payment-status', async (req, res) => {
const order = await findOrderVisibleToCurrentUser(
req.params.orderId,
req.user
);
if (!order) {
return res.status(404).json({
error: 'Order not found'
});
}
return res.json({
orderId: order.id,
paymentStatus: order.paymentStatus
});
});
配置服务器回退到 AngularJS 入口页
在 HTML5 路由模式下,浏览器访问:
GET /booking-success?orderId=123
请求仍会先到达服务器。服务器需要为这个 GET 返回 AngularJS 应用的 index.html,然后由 $routeProvider 渲染成功页面。
Express 可以在 API 和支付回调路由之后添加以下配置:
const path = require('path');
app.use(express.static(path.join(__dirname, 'public')));
// 必须放在 API、支付回调和静态资源路由之后。
app.get('*', function (req, res) {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
Nginx 的典型配置如下:
location / {
try_files $uri $uri/ /index.html;
}
SPA 回退规则不能截获支付回调接口。以下请求必须由后端接口处理:
POST /api/payments/callback
POST /payment/return
使用 Hash 路由时的处理方式
如果 AngularJS 使用默认的 Hash 路由,成功页地址可能是:
https://xyz.com/#/booking-success?orderId=123
后端可以这样重定向:
res.redirect(
303,
'/#/booking-success?orderId=' + encodeURIComponent(orderId)
);
URL 中 # 后面的内容不会发送给服务器。因此,Payment Gateway 的 Callback URL 不能设置为:
https://xyz.com/#/payment-callback
服务器实际只能收到对 / 的请求,无法根据 Fragment 处理支付回调。Callback URL 必须指向真实的后端地址,例如:
https://xyz.com/api/payments/callback
区分服务端通知和浏览器回跳
支付平台通常会提供两类 URL:
notify_url、webhook_url或 Callback URL:支付平台服务器向商户服务器发送通知。return_url、success_url:支付完成后,把用户浏览器带回商户页面。
服务端通知不一定会把用户浏览器带回来。支付平台可能只在后台向 Callback URL 发送 HTTP 请求,并要求接口返回固定文本或指定状态码。这种情况下,即使后端返回重定向,执行跳转的也是支付平台的 HTTP 客户端,而不是用户浏览器。
如果平台同时支持这两类地址,应分别配置:
Callback URL:
https://xyz.com/api/payments/callback
Return URL:
https://xyz.com/booking-success
具体名称、请求格式和回调响应要求,应以相应支付平台的接口文档为准。
安全和可靠性注意事项
支付成功页不能只根据回调参数或 URL 参数判断付款是否成功。例如,用户可以自行构造以下地址:
/booking-success?status=paid&orderId=123
处理支付回调时,至少需要注意以下事项:
- 按照支付平台规定的方式验证签名。
- 校验支付金额、币种、商户号和订单号。
- 如果条件允许,通过支付平台的服务端 API 再次查询交易状态。
- 回调处理必须支持幂等。同一通知重复到达时,不能重复发货或重复记账。
- 订单状态应保存在后端,不能只存放在 AngularJS 内存或
localStorage中。 - 不要把签名密钥、API Secret 等凭据放入 AngularJS 前端代码。
- Callback URL 应使用 HTTPS。
- 成功页应从后端查询状态,并处理”支付结果确认中”的情况。
- 如果回跳依赖登录 Cookie,需要检查 Cookie 的
SameSite设置。跨站表单 POST 可能不会携带设置为SameSite=Lax或Strict的 Cookie。
整体结构是:后端接收并验证支付回调,AngularJS 通过 GET 打开结果页面,再展示后端保存的订单状态。
备注:内容仅供参考。
