如何使用 HttpClient 提交表单并跳转支付网站?
结论
HttpClient 不能直接让用户的浏览器跳转到支付网站。
HttpClient.PostAsync() 在 Web 服务器上执行,建立的是“你的服务器 → 支付网关”的请求。即使支付网关返回 302,也只是 HttpClient 在服务器端处理跳转,用户的浏览器并不知道这件事。
要实现浏览器提交表单的效果,通常有两种做法:
- 按照支付网关的接口要求,先在服务器端创建支付会话,再将浏览器重定向到网关返回的支付 URL。
- 如果网关要求浏览器通过 POST 提交表单字段,就向浏览器返回一个自动提交的 HTML 表单。
推荐方式:创建支付会话后重定向
如果支付网关提供“创建订单或支付会话”接口,可以先用 HttpClient 调用该接口,再从响应中取出支付页面地址:
private async Task StartPaymentAsync()
{
using (var client = new HttpClient())
{
var values = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(
"token",
"MyTokenParameter"
),
new KeyValuePair<string, string>(
"postBackURL",
"https://mywebsite.com/status.aspx"
)
};
using (var content = new FormUrlEncodedContent(values))
using (var response = await client.PostAsync(
"https://somewebsite.com/api/create-payment",
content))
{
response.EnsureSuccessStatusCode();
var responseString =
await response.Content.ReadAsStringAsync();
// 具体解析方式取决于支付网关的响应格式。
// 例如响应 JSON 中可能包含 paymentUrl。
var paymentUrl = ParsePaymentUrl(responseString);
Response.Redirect(paymentUrl, false);
Context.ApplicationInstance.CompleteRequest();
}
}
}
在这个流程中,HttpClient 只负责创建支付会话,支付页面最终仍由用户浏览器打开。
ParsePaymentUrl() 只是示例,具体实现要根据网关实际返回的 JSON、XML 或其他格式来写,不能假定响应中一定有这个字段。
网关要求浏览器 POST 时:返回自动提交表单
有些支付网关不提供可以直接访问的支付 URL,而是要求用户浏览器通过 POST 提交 token、订单号等字段。这时不能使用普通的 Response.Redirect(),因为重定向通常会发起 GET 请求,不会携带原表单中的数据。
可以返回一个自动提交的表单:
using System;
using System.Web;
private void RedirectToPaymentGateway()
{
var actionUrl = "https://somewebsite.com/someurl";
var token = "MyTokenParameter";
var postBackUrl = "https://mywebsite.com/status.aspx";
var html = $@"<!doctype html>
<html lang=""zh-CN"">
<head>
<meta charset=""utf-8"">
<title>正在跳转到支付页面</title>
</head>
<body>
<p>正在跳转到支付页面,请稍候……</p>
<form id=""paymentForm""
method=""post""
action=""{HttpUtility.HtmlAttributeEncode(actionUrl)}"">
<input type=""hidden""
name=""token""
value=""{HttpUtility.HtmlAttributeEncode(token)}"">
<input type=""hidden""
name=""postBackURL""
value=""{HttpUtility.HtmlAttributeEncode(postBackUrl)}"">
<noscript>
<button type=""submit"">继续支付</button>
</noscript>
</form>
<script>
document.getElementById('paymentForm').submit();
</script>
</body>
</html>";
Response.Clear();
Response.ContentType = "text/html; charset=utf-8";
Response.Write(html);
Context.ApplicationInstance.CompleteRequest();
}
这样,支付表单会在用户浏览器中提交,支付网站可以正常接管当前页面。
动态写入 HTML 的字段必须经过 HtmlAttributeEncode()。否则,订单信息或其他外部数据可能破坏 HTML,甚至造成 XSS 漏洞。
如果支付网关返回 302
可以关闭 HttpClient 的自动重定向,然后读取响应中的 Location:
private async Task ProcessAsyncRequest()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = false
};
using (var client = new HttpClient(handler))
{
var values = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(
"token",
"MyTokenParameter"
),
new KeyValuePair<string, string>(
"postBackURL",
"https://mywebsite.com/status.aspx"
)
};
using (var content = new FormUrlEncodedContent(values))
using (var response = await client.PostAsync(
"https://somewebsite.com/someurl",
content))
{
if ((int)response.StatusCode >= 300 &&
(int)response.StatusCode < 400 &&
response.Headers.Location != null)
{
var redirectUrl = response.Headers.Location;
// 只允许跳转到预先认可的支付网关域名。
if (!IsAllowedPaymentUrl(redirectUrl))
{
throw new InvalidOperationException(
"支付网关返回了不受信任的跳转地址。"
);
}
Response.Redirect(redirectUrl.ToString(), false);
Context.ApplicationInstance.CompleteRequest();
return;
}
var responseString =
await response.Content.ReadAsStringAsync();
Response.Write(
HttpUtility.HtmlEncode(responseString)
);
}
}
}
这种做法有一个前提:支付网关返回的 Location 必须能由浏览器通过 GET 直接访问。
如果服务器端 POST 创建的支付状态依赖 HttpClient 保存的 Cookie,仅把 URL 交给浏览器仍可能失败,因为这些服务器端 Cookie 不会自动进入用户浏览器。遇到这种情况,应使用网关正式提供的支付会话流程,或者让浏览器直接提交表单。
为什么直接输出响应内容通常不可靠
下面的代码只会把支付网关返回的响应正文写入当前页面:
var responseString =
await response.Content.ReadAsStringAsync();
Response.Write(responseString);
它不会改变浏览器地址,也不会把支付网关的 Cookie、重定向状态或请求上下文传递给浏览器。
即使响应正文刚好是一份完整的 HTML,也可能受相对 URL、Cookie、CSP、跨域脚本或表单来源校验影响而无法正常运行。因此,不建议把支付页面作为反向代理内容直接输出。
支付接入注意事项
- 支付流程和回调地址应使用 HTTPS。
postBackURL通常是服务器通知地址,不一定是用户完成支付后的浏览器返回地址。两者应按照网关文档分别配置。- 收到回调不代表支付已经成功。必须校验签名、订单号、金额、币种和支付状态。
- 不要把密钥、私钥或只供服务器使用的凭据放进浏览器隐藏字段。
- 对网关返回的跳转 URL 进行域名白名单校验,避免产生开放重定向漏洞。
- 不要只依赖用户返回页面来更新订单状态,因为用户可能直接关闭页面。订单状态应以经过签名验证的服务端通知或主动查询结果为准。
简而言之,HttpClient 适合服务器之间的调用。需要让用户进入支付页面时,还要向浏览器返回重定向,或者让浏览器提交支付表单。
备注:内容仅供参考。
