外部支付完成后通过 POST 重定向到前端 URL
结论
不要让支付平台直接向 Vue 路由发送 POST 请求。应将 return_url 指向 Java 后端公开的 POST endpoint。后端负责解析参数、校验签名并确认支付状态,再返回 303 See Other,将浏览器重定向到 Vue 的支付结果页面。
VueJS 运行在浏览器中,并不是 HTTP 服务器,不能像 Java backend 那样监听外部 POST 请求。即使前端页面由 Nginx 等服务器托管,Vue Router 也只能在页面加载后处理客户端路由,无法接收支付回调。
推荐流程
可以按以下流程处理:
- Vue 前端请求 Java 后端创建支付订单。
- 后端调用支付平台,并将支付页面 URL 返回给前端。
- 前端通过
window.location.href跳转到支付页面。 - 支付完成后,支付平台向 Java 后端配置的
return_url发起POST请求。 - Java 后端校验请求,并主动查询支付平台,确认订单状态。
- 后端返回
303,将用户浏览器重定向到 Vue 结果页面。 - Vue 使用订单编号向后端查询最终支付状态。
例如:
POST https://api.example.com/payments/return
|
| 验签并确认支付状态
v
303 Location: https://app.example.com/payment/result?orderId=123
|
v
GET https://api.example.com/payments/123/status
Java 后端处理示例
以下示例使用 Spring MVC。支付平台发送的是表单、JSON 还是其他格式,应以其接口文档为准。
如果平台发送 application/x-www-form-urlencoded:
@RestController
@RequestMapping("/payments")
public class PaymentReturnController {
private final PaymentService paymentService;
public PaymentReturnController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping(
value = "/return",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public ResponseEntity<Void> handleReturn(
@RequestParam MultiValueMap<String, String> parameters) {
PaymentResult result = paymentService.verifyAndProcess(parameters);
URI frontendUri = UriComponentsBuilder
.fromUriString("https://app.example.com/payment/result")
.queryParam("orderId", result.orderId())
.build()
.toUri();
return ResponseEntity
.status(HttpStatus.SEE_OTHER)
.location(frontendUri)
.build();
}
}
如果支付平台发送 JSON,可以将 endpoint 改为:
@PostMapping(
value = "/return",
consumes = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Void> handleReturn(
@RequestBody PaymentReturnRequest request,
@RequestHeader HttpHeaders headers) {
PaymentResult result = paymentService.verifyAndProcess(request, headers);
URI frontendUri = UriComponentsBuilder
.fromUriString("https://app.example.com/payment/result")
.queryParam("orderId", result.orderId())
.build()
.toUri();
return ResponseEntity
.status(HttpStatus.SEE_OTHER)
.location(frontendUri)
.build();
}
这里使用 303 See Other,是为了明确要求客户端使用 GET 请求访问 Location 指定的页面。某些其他重定向状态码可能会保留原来的 POST 方法,导致客户端再次向前端地址发送 POST 请求。
Vue 结果页面
前端结果页面不应直接相信 URL 中的 success=true、金额或支付状态,而应根据订单编号向后端查询。
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
export default {
setup() {
const route = useRoute()
const status = ref('loading')
onMounted(async () => {
const orderId = route.query.orderId
if (!orderId) {
status.value = 'invalid'
return
}
try {
const response = await axios.get(
`/api/payments/${encodeURIComponent(orderId)}/status`
)
status.value = response.data.status
} catch (error) {
status.value = 'unknown'
}
})
return { status }
}
}
后端状态查询接口可以只返回受控数据:
{
"orderId": "123",
"status": "PAID"
}
分开配置 return_url 与 webhook
部分支付平台会区分两种回调:
return_url:用户完成支付后,通过浏览器返回商户网站。- webhook 或 notification URL:支付平台服务器异步通知商户服务器。
如果平台支持 webhook,应主要通过 webhook 或后端主动查询来确认支付结果,不能只依赖用户浏览器访问 return_url。用户可能关闭页面、遇到网络中断,或者没有完成返回跳转。
可以分别配置:
return_url:
https://api.example.com/payments/return
webhook_url:
https://api.example.com/payments/webhook
webhook endpoint 通常只需返回成功状态,不必重定向:
@PostMapping("/webhook")
public ResponseEntity<Void> handleWebhook(
@RequestBody String body,
@RequestHeader HttpHeaders headers) {
paymentService.verifyAndProcessWebhook(body, headers);
return ResponseEntity.ok().build();
}
具体的响应内容、状态码和重试规则,必须遵循对应支付平台的文档。
安全和实现注意事项
验证支付结果
不能仅凭请求来自约定的 URL,或者参数中包含 status=success,就将订单标记为已支付。后端至少应执行以下一种或多种验证:
- 校验支付平台提供的数字签名。
- 验证订单号、商户号、币种和金额。
- 调用支付平台的服务端 API 查询交易状态。
- 确认交易对应的内部订单存在。
- 拒绝支付金额与订单金额不一致的请求。
保证幂等性
支付平台可能重复发送通知,用户也可能刷新返回页面。订单处理逻辑必须允许同一笔交易被处理多次,同时避免重复扣减库存、发货或记账。
例如,可以将支付平台交易号设置为唯一键,并且只允许订单状态从 PENDING 转换为 PAID 一次。
不要在重定向 URL 中放置敏感数据
重定向到前端时,通常只需携带内部订单编号或一次性查询标识:
https://app.example.com/payment/result?orderId=123
不要在 URL 中加入签名密钥、银行卡信息、完整支付凭据或能够直接修改订单状态的 token。URL 可能被记录在浏览器历史、代理日志和分析系统中。
区分浏览器回跳和服务端通知
如果支付平台通过浏览器表单向 POST return_url 提交请求,后端返回 303 通常能将用户带回 Vue 页面。如果这是一个纯服务端请求,重定向只会被支付平台服务器接收,用户浏览器的位置不会改变。
遇到这种情况,需要另外配置供浏览器访问的成功页面,或者按照支付平台的要求,分别设置 browser return URL 和 webhook URL。
检查 CSRF 配置
支付平台无法携带应用生成的 CSRF token,因此 Spring Security 可能会拒绝这个外部 POST 请求。可以只针对确切的支付回调路径使用适合第三方回调的安全策略,但不能关闭整个应用的 CSRF 防护。
这个 endpoint 仍然需要通过支付平台签名、请求认证或服务端查询来验证请求的真实性。
Vue 只负责展示支付结果。支付回调、签名校验和订单状态变更都应由 Java 后端处理。
备注:内容仅供参考。
