Angular 7 使用 httpClient POST 对接 Payment gateway
结论
支付网关通常不适合通过 Angular HttpClient 直接调用。更稳妥的做法是使用原生 HTML 表单,以整页 POST 的方式跳转;也可以先由自己的后端创建支付请求,再让浏览器跳转到网关。
支付完成后的 POST 回调同样不能直接交给 Angular 路由。Angular 是运行在浏览器中的前端程序,无法监听外部 HTTP POST 请求。你需要提供一个公开的后端回调地址,由后端接收请求、验证签名并处理支付结果,最后重定向到 Angular 的成功或失败页面。
为什么请求会落到 localhost
下面使用的是相对 URL:
this.http.post('direcPayment/', formData, options)
浏览器会按照当前站点解析它,例如:
http://localhost:4200/direcPayment/
在 Angular 本地开发中,这种写法可以配合开发代理使用,但启动开发服务器时必须加载代理配置:
ng serve --proxy-config proxy.config.json
也可以在 angular.json 的 serve.options 中设置 proxyConfig。
请求路径最好以 / 开头:
this.http.post('/direcPayment', body, options)
还要注意以下几点:
- Angular 开发代理只在本地开发环境中生效。
- 部署到生产环境后,需要使用 Nginx、Apache、API Gateway 或自己的后端提供反向代理。
target应指向网关的实际协议和主机,例如https://payment.example.com。pathRewrite处理后的路径必须与网关的真实接口路径一致,不能根据代理名称自行推断。
不要在请求中添加 CORS 响应头
下面这些头不应该由 Angular 设置:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
它们是服务器返回给浏览器的 CORS 响应头,并不是客户端请求头。把它们添加到请求中无法解决跨域问题,还可能引发额外的预检请求。
另外,HttpHeaders 是不可变对象,调用 append() 不会修改原对象:
let headers = new HttpHeaders();
headers = headers.append('X-Custom-Header', 'value');
不过在这个场景下,通常不需要手动设置上述 CORS 头。
如果网关允许 HttpClient 调用
只有当支付网关明确支持浏览器跨域请求,并说明支付请求可以通过 AJAX/XHR 发起时,才适合使用 HttpClient。
先确认网关要求的 Content-Type。如果接口要求普通表单编码,一般使用:
application/x-www-form-urlencoded
这时不要使用 FormData,因为它会生成 multipart/form-data 请求。
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
formSubmit(encryptedString: string): void {
const requestParameter =
`MerchantId||CollaboratorID||${encryptedString}`;
const body = new HttpParams()
.set('requestParameter', requestParameter);
this.http.post('/direcPayment', body.toString(), {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
}),
responseType: 'text'
}).subscribe({
next: response => {
console.log(response);
},
error: error => {
console.error(error);
}
});
}
如果传入的 encryptedString 已经是完整的:
MerchantId||CollaboratorID||Encrypted_string
就不要再次拼接,直接把它作为 requestParameter 发送。
即使请求成功,HttpClient 发起的也只是后台 XHR 请求,不会像原生表单一样将当前页面导航到支付网关。许多网关还会返回 HTML 页面或进行连续重定向。这类流程常受 CORS、Cookie、SameSite 和第三方会话限制,一般不适合使用 HttpClient。
推荐使用原生表单跳转
如果网关文档要求“POST 到支付页面”,可以使用原生表单:
<form
ngNoForm
method="post"
action="https://payment.example.com/direcPayment"
>
<input
type="hidden"
name="requestParameter"
[value]="requestParameter"
>
<button type="submit">前往支付</button>
</form>
在组件中准备完整参数:
requestParameter = `MerchantId||CollaboratorID||${this.encryptedString}`;
需要通过代码动态提交时,可以创建一个临时表单:
redirectToGateway(encryptedString: string): void {
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://payment.example.com/direcPayment';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'requestParameter';
input.value = `MerchantId||CollaboratorID||${encryptedString}`;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
这样会触发真正的页面导航,能够兼容传统支付页面,也不依赖 AJAX CORS。
Angular 不能直接接收支付网关的 POST
例如,下面的 Angular 路由:
{
path: 'payment/success',
component: PaymentSuccessComponent
}
它只能在浏览器已经加载 Angular 应用后处理客户端导航,无法读取外部服务提交给服务器的 POST 请求体。
支付回调应该指向后端,例如:
POST https://api.example.com/payment/callback
后端需要完成以下处理:
- 读取网关提交的加密参数。
- 验证签名并解密。
- 核对订单号、金额、币种和商户信息。
- 更新订单状态。
- 返回网关要求的确认响应。
- 如果请求属于浏览器回跳,再重定向到 Angular 的 GET 页面。
下面是一个 Express 示例:
import express from 'express';
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.post('/payment/callback', async (req, res) => {
const encryptedData = req.body.requestParameter;
if (!encryptedData) {
return res.status(400).send('Missing requestParameter');
}
try {
// 按支付网关文档完成解密、验签和订单核对。
const paymentResult = await verifyPaymentResult(encryptedData);
// 更新操作应具备幂等性,避免重复回调造成重复处理。
await updateOrderStatus(paymentResult);
const orderId = encodeURIComponent(paymentResult.orderId);
return res.redirect(
303,
`https://www.example.com/payment/result?orderId=${orderId}`
);
} catch (error) {
return res.status(400).send('Invalid payment response');
}
});
Angular 的结果页面只接收订单标识,再向自己的后端查询最终状态:
this.http
.get(`/api/orders/${orderId}/payment-status`)
.subscribe(status => {
this.paymentStatus = status;
});
不要把回调中的加密数据、支付成功标志或金额直接交给 Angular,并将它们当作可信结果。
回调需要区分两种情况
支付网关所说的 POST 回调可能指两种请求:
- 浏览器回跳:支付页面通过表单 POST 到指定地址,请求由用户浏览器发出。
- 服务端通知:支付网关服务器主动请求商户服务器,也叫 webhook 或异步通知。
两种请求都应该先由后端接收。处理服务端通知后,通常需要直接返回网关规定的确认文本。处理浏览器回跳后,可以使用 303 See Other 跳转到 Angular 页面。具体响应格式应以支付网关的接口文档为准。
还需要注意
- 回调地址必须是支付网关可以访问的公网 HTTPS 地址,正式环境不能使用
localhost:4200。 - 商户密钥、解密密钥和签名密钥不能放在 Angular 代码中。
- 不能只根据成功页面判断支付是否成功,应以后端验签结果或服务端查询结果为准。
- 网关可能重复发送回调,因此订单更新必须具备幂等性。
- “Session expired” 很可能只是网关的通用错误页面,无法证明问题一定出在会话。还需要检查字段格式、编码方式、接口路径、商户编号、签名、时间戳,以及测试环境和生产环境是否匹配。
- 如果网关要求
multipart/form-data,可以继续使用FormData,但不要手动设置Content-Type。浏览器会自动添加正确的 boundary。最终使用哪种编码格式,应以网关文档为准。
备注:内容仅供参考。
