PAYATHON 2026

在 Asp.net 中仅对部分参数使用 POST method

支付老李

结论

不要把包含 20 多个输入框的完整表单直接提交给支付网关。建议按以下流程处理:

  1. 用户先把完整表单提交到自己的 ASP.NET 服务端。
  2. 服务端提取支付网关所需的字段。
  3. 服务端校验金额、创建订单并生成签名。
  4. 服务端发起独立的 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 和当前接口文档构造请求。

原表单用于收集业务数据。支付请求则由服务端重新构造,只把白名单中的必要参数发送给支付网关。

备注:内容仅供参考。