在 Asp.net 中仅对部分参数使用 POST method
结论
不要把包含 20 多个输入框的完整表单直接提交给支付网关。建议按以下流程处理:
- 用户先把完整表单提交到自己的 ASP.NET 服务端。
- 服务端提取支付网关所需的字段。
- 服务端校验金额、创建订单并生成签名。
- 服务端发起独立的 POST 请求,或者生成一个只包含必要隐藏字段的表单,再提交给支付网关。
浏览器提交表单时,会发送表单中所有具有 name、未被禁用且符合提交条件的控件。即使 ASP.NET 服务端只读取其中一部分字段,浏览器也不会自动减少实际发送的参数。
推荐方案:在服务端构造支付请求
支付金额、订单号、回调地址和签名等数据不能直接信任浏览器提交的值。服务端应重新获取或计算这些数据,并且只向支付网关发送接口要求的参数。
以下是一个 ASP.NET Core 示例:
using System.Net.Http;
using Microsoft.AspNetCore.Mvc;
public class PaymentController : Controller
{
private readonly HttpClient _httpClient;
public PaymentController(HttpClient httpClient)
{
_httpClient = httpClient;
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> CreateOrder(CheckoutViewModel model)
{
if (!ModelState.IsValid)
{
return View("Checkout", model);
}
// 应根据商品和订单数据在服务端计算金额,
// 不要直接信任客户端提交的金额。
decimal amount = await CalculateAmountAsync(model.ProductId);
var paymentParameters = new Dictionary<string, string>
{
["merchant_id"] = "your-merchant-id",
["order_id"] = CreateOrderNumber(),
["amount"] = amount.ToString("0.00",
System.Globalization.CultureInfo.InvariantCulture),
["return_url"] = "https://example.com/payment/return"
};
paymentParameters["signature"] = CreateSignature(paymentParameters);
using var content = new FormUrlEncodedContent(paymentParameters);
HttpResponseMessage response = await _httpClient.PostAsync(
"https://payment.example.com/pay",
content);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
return Content(responseBody, "text/html");
}
private Task<decimal> CalculateAmountAsync(int productId)
{
// 示例代码:实际项目中应从数据库或订单服务获取价格。
return Task.FromResult(100.00m);
}
private string CreateOrderNumber()
{
return Guid.NewGuid().ToString("N");
}
private string CreateSignature(
IReadOnlyDictionary<string, string> parameters)
{
// 按支付网关规定的字段顺序、编码和算法生成签名。
throw new NotImplementedException();
}
}
注册 HttpClient:
builder.Services.AddHttpClient();
这样只会把 paymentParameters 中明确加入的字段发送给支付网关。其他文本框的内容仍保留在自己的应用中,不会进入支付请求。
有些支付网关要求浏览器跳转到网关的支付页面,不能由服务端直接接收最终 HTML。遇到这种情况,可以生成一个独立的支付表单。
需要跳转到支付页面时:生成独立表单
服务端完成订单验证并生成签名后,可以返回一个只包含必要字段的 HTML 表单,然后自动提交:
@model PaymentRedirectViewModel
<form id="paymentForm"
method="post"
action="https://payment.example.com/pay">
<input type="hidden" name="merchant_id" value="@Model.MerchantId" />
<input type="hidden" name="order_id" value="@Model.OrderId" />
<input type="hidden" name="amount" value="@Model.Amount" />
<input type="hidden" name="return_url" value="@Model.ReturnUrl" />
<input type="hidden" name="signature" value="@Model.Signature" />
<noscript>
<button type="submit">前往支付</button>
</noscript>
</form>
<script>
document.getElementById("paymentForm").submit();
</script>
这个表单与原来的业务表单彼此独立,提交时只会包含代码中列出的隐藏字段。
不要把第二个 <form> 嵌套在原表单中。HTML 不支持嵌套表单,不同浏览器的解析结果也可能不一致。ASP.NET Web Forms 页面通常只能使用一个服务端表单,因此更适合由服务端生成单独的跳转页面。
ASP.NET Web Forms 的处理方式
在 Web Forms 中,可以先处理原页面提交的数据,再生成一个只包含支付字段的自动提交表单:
protected void PayButton_Click(object sender, EventArgs e)
{
string orderId = CreateOrder();
decimal amount = GetOrderAmount(orderId);
var parameters = new Dictionary<string, string>
{
["merchant_id"] = "your-merchant-id",
["order_id"] = orderId,
["amount"] = amount.ToString(
"0.00",
System.Globalization.CultureInfo.InvariantCulture),
["return_url"] = "https://example.com/payment/return"
};
parameters["signature"] = CreateSignature(parameters);
string html = BuildPaymentForm(
"https://payment.example.com/pay",
parameters);
Response.Clear();
Response.ContentType = "text/html";
Response.Write(html);
Context.ApplicationInstance.CompleteRequest();
}
生成表单时,必须对内容进行 HTML 编码:
private static string BuildPaymentForm(
string gatewayUrl,
IDictionary<string, string> parameters)
{
var html = new System.Text.StringBuilder();
html.Append("<!doctype html><html><body>");
html.AppendFormat(
"<form id=\"paymentForm\" method=\"post\" action=\"{0}\">",
System.Web.HttpUtility.HtmlAttributeEncode(gatewayUrl));
foreach (var parameter in parameters)
{
html.AppendFormat(
"<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />",
System.Web.HttpUtility.HtmlAttributeEncode(parameter.Key),
System.Web.HttpUtility.HtmlAttributeEncode(parameter.Value));
}
html.Append("</form>");
html.Append(
"<script>document.getElementById('paymentForm').submit();</script>");
html.Append("</body></html>");
return html.ToString();
}
代码中的参数名称、签名规则和支付地址仅供示例。实际项目必须按照所接入支付网关的接口文档设置。
能否通过禁用控件来减少提交字段
设置为 disabled 的表单控件不会随表单提交:
<input type="text" name="remark" disabled />
没有 name 属性的普通 HTML 控件通常也不会被提交:
<input type="text" id="remark" />
但不建议把这种方式作为支付请求的主要实现方案,原因如下:
- JavaScript 可以重新启用控件或修改字段。
- 用户可以通过开发者工具自行构造请求。
- 在 ASP.NET Web Forms 中,禁用服务器控件还可能影响回发和页面状态。
- 这只能减少浏览器提交的字段,无法提供安全保证。
如果确实需要浏览器直接提交,可以使用 JavaScript 动态创建一个独立表单:
function submitPayment(paymentData) {
const form = document.createElement("form");
form.method = "post";
form.action = "https://payment.example.com/pay";
for (const [name, value] of Object.entries(paymentData)) {
const input = document.createElement("input");
input.type = "hidden";
input.name = name;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}
即使采用这种方式,paymentData 中的金额、订单号和签名也必须由服务端生成,不能只在浏览器中计算。
注意事项
- 不要把商户密钥、API 密钥或签名密钥写入 HTML、JavaScript、隐藏字段或 ViewState。
- 金额、商品信息和订单状态必须在服务端校验,因为隐藏字段同样可以被修改。
- 生成签名时,必须严格遵循支付网关规定的字段排序、字符编码和摘要算法。
- 使用 HTTPS,并验证支付网关发出的服务端异步通知。不能仅凭用户跳转到成功页面就认定支付已经完成。
- 订单号必须唯一。处理支付通知时还要保证幂等,避免重复更新订单。
- 如果网关提供官方 SDK,应按照官方 SDK 和当前接口文档构造请求。
原表单用于收集业务数据。支付请求则由服务端重新构造,只把白名单中的必要参数发送给支付网关。
备注:内容仅供参考。
