PAYATHON 2026

如何在 ASP.NET C# 2.0 中集成 Payment Gateway

支付阿杰

明确结论

不建议自行开发完整的 Payment Gateway。更合适的做法是选择支付服务商或收单机构提供的 merchant account,并通过其托管支付页面或支付 API 完成集成。

对于 ASP.NET 2.0,建议优先跳转到第三方托管收银台。网站只负责创建订单和发起跳转,不直接接触银行卡号、CVV 等敏感信息。这种方式实现起来更简单,也能明显缩小 PCI DSS 的合规范围。

ASP.NET 2.0 和早期 .NET Framework 已经过时,可能无法满足支付服务商对 TLS 1.2、加密算法和 SDK 运行环境的要求。如果必须保留旧系统,可以增加一个运行在受支持 .NET 版本上的独立支付服务,再由 ASP.NET 2.0 调用该服务。

Payment Gateway 的基本流程

常见的支付流程如下:

  1. 用户在网站提交订单。
  2. 服务器生成唯一订单号,并将订单保存为 Pending 状态。
  3. 服务器向支付服务商创建支付请求,提交订单号、金额、币种和回调地址。
  4. 用户跳转到第三方托管支付页面。
  5. 用户在支付服务商的页面填写银行卡信息并完成验证。
  6. 支付服务商将用户浏览器跳回网站。
  7. 支付服务商同时向网站的服务器回调地址发送支付通知。
  8. 网站验证通知签名,或再次查询支付状态。
  9. 确认支付成功后,将订单更新为 Paid,然后发货或开通服务。

浏览器返回页面只适合展示支付结果,不能作为确认付款的唯一依据。用户可能在返回前关闭浏览器,请求参数也可能被伪造。订单状态应以验证通过的服务器通知或支付查询结果为准。

开始集成前需要准备什么

先向支付服务商取得以下资料:

  • merchant account 或 merchant ID
  • 测试环境和正式环境地址
  • API key、共享密钥或客户端证书
  • 创建支付、查询支付和退款接口文档
  • 返回地址和服务器通知地址的要求
  • 签名生成及验证规则
  • 支持的 TLS 版本、币种和金额单位
  • 测试卡号及沙箱测试方法

各家支付服务商使用的字段、签名算法和状态名称并不相同,不能用一套通用参数代替官方文档。

推荐方案:跳转到托管支付页面

如果服务商支持通过 HTML 表单创建支付,可以由服务器生成必要字段,再让浏览器将表单提交到其收银台。下面只展示基本结构,字段名和签名规则必须按服务商的实际规范替换。

<form id="paymentForm" method="post"
      action="https://sandbox.example-payment.com/checkout">
    <input type="hidden" name="merchant_id"
           value="<%= Server.HtmlEncode(MerchantId) %>" />
    <input type="hidden" name="order_id"
           value="<%= Server.HtmlEncode(OrderId) %>" />
    <input type="hidden" name="amount"
           value="<%= Server.HtmlEncode(AmountText) %>" />
    <input type="hidden" name="currency" value="USD" />
    <input type="hidden" name="return_url"
           value="https://www.example.com/payment/return.aspx" />
    <input type="hidden" name="notify_url"
           value="https://www.example.com/payment/notify.aspx" />
    <input type="hidden" name="signature"
           value="<%= Server.HtmlEncode(Signature) %>" />

    <button type="submit">立即支付</button>
</form>

订单号和金额必须来自服务器端保存的订单数据,不能信任浏览器提交回来的金额。签名也必须在服务器端计算,密钥不能写入 HTML、JavaScript 或查询字符串。

金额最好使用最小货币单位,例如分:

decimal amount = 19.95m;
long amountInCents = Decimal.ToInt64(
    Decimal.Round(amount * 100m, 0, MidpointRounding.AwayFromZero)
);

不要使用 double 保存金额,以免产生浮点精度误差。

在 ASP.NET 2.0 中调用支付 API

如果服务商提供 HTTPS API,可以使用 HttpWebRequest。以下代码只展示基本调用方式,并不对应任何特定支付平台的真实接口。

using System;
using System.IO;
using System.Net;
using System.Text;

public static class PaymentClient
{
    public static string CreatePayment(
        string endpoint,
        string merchantId,
        string orderId,
        long amountInCents,
        string signature)
    {
        string body =
            "merchant_id=" + Uri.EscapeDataString(merchantId) +
            "&order_id=" + Uri.EscapeDataString(orderId) +
            "&amount=" + amountInCents.ToString(
                System.Globalization.CultureInfo.InvariantCulture) +
            "&currency=USD" +
            "&signature=" + Uri.EscapeDataString(signature);

        byte[] data = Encoding.UTF8.GetBytes(body);

        HttpWebRequest request =
            (HttpWebRequest)WebRequest.Create(endpoint);

        request.Method = "POST";
        request.ContentType = "application/x-www-form-urlencoded";
        request.ContentLength = data.Length;
        request.Timeout = 30000;

        using (Stream requestStream = request.GetRequestStream())
        {
            requestStream.Write(data, 0, data.Length);
        }

        using (HttpWebResponse response =
            (HttpWebResponse)request.GetResponse())
        using (Stream stream = response.GetResponseStream())
        using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
        {
            return reader.ReadToEnd();
        }
    }
}

生产代码还要处理超时、非 2xx 响应、响应解析、安全日志和重复请求。遇到 HTTPS 错误时,不要关闭证书验证,也不要实现“接受所有证书”的回调。

正确处理支付通知

通知处理程序至少需要完成以下检查:

  1. 验证服务商的签名、MAC 或证书。
  2. 检查 merchant ID 是否属于当前系统。
  3. 根据订单号从数据库读取订单。
  4. 比较通知中的金额、币种和原订单。
  5. 确认支付状态确实为成功。
  6. 必要时调用支付查询接口进行二次确认。
  7. 使用数据库事务更新订单状态。
  8. 返回服务商规定的确认响应。

处理程序必须具备幂等性。支付服务商可能因为网络超时而多次发送同一条通知,重复通知不能导致重复发货、重复记账或重复充值。

伪代码结构如下:

protected void Page_Load(object sender, EventArgs e)
{
    string orderId = Request.Form["order_id"];
    string paymentId = Request.Form["payment_id"];
    string status = Request.Form["status"];
    string signature = Request.Form["signature"];

    if (!VerifySignature(Request.Form, signature))
    {
        Response.StatusCode = 400;
        Response.End();
        return;
    }

    Order order = OrderRepository.FindByOrderId(orderId);

    if (order == null || !PaymentMatchesOrder(Request.Form, order))
    {
        Response.StatusCode = 400;
        Response.End();
        return;
    }

    if (status == "SUCCESS" && !order.IsPaid)
    {
        OrderRepository.MarkAsPaid(orderId, paymentId);
    }

    Response.Write("OK");
}

VerifySignature、成功状态值和响应内容必须严格按照具体服务商的文档实现。不能只看到 status=SUCCESS 就更新订单。

SSL、TLS 与旧版 .NET 的限制

网站应使用受信任机构签发的 HTTPS 证书,但安装证书并不等于支付集成已经安全。还要确认:

  • 整个结账流程均使用 HTTPS
  • Cookie 设置了 Secure 和适当的 HttpOnly
  • API 密钥保存在受保护的服务器配置中
  • 日志不记录卡号、CVV、密钥或完整支付报文
  • 数据库不保存 CVV
  • 如果没有必要,卡号不进入应用程序和服务器
  • 后台管理和退款操作有权限控制及审计记录

.NET Framework 2.0 对现代 TLS 的支持是主要风险之一。能否建立 TLS 1.2 连接,取决于操作系统、补丁、运行时配置和支付服务商的要求,不能只看代码。如果服务商要求现代 SDK 或强制使用 TLS 1.2,迁移到受支持的 .NET 版本通常比继续兼容旧运行时更可靠。

更稳妥的旧系统改造方式

如果暂时无法升级 ASP.NET 2.0,可以使用隔离架构:

ASP.NET 2.0 网站
        |
        | 内部认证请求
        v
现代 .NET 支付服务
        |
        | HTTPS / TLS 1.2+
        v
第三方 Payment Gateway

现代支付服务负责创建支付、验签、查询和退款,旧网站只处理订单和页面跳转。内部接口同样需要身份认证、请求签名、重放防护和访问限制,不能因为它属于内部服务就省略这些安全措施。

上线前检查

至少测试以下场景:

  • 支付成功、失败和用户取消
  • 浏览器未跳回,但服务器通知成功
  • 通知重复发送或乱序到达
  • 金额、币种或签名被篡改
  • API 超时以及结果未知
  • 同一订单被重复点击支付
  • 已支付订单再次收到成功通知
  • 退款、部分退款及退款失败
  • 沙箱和正式环境配置完全隔离

最实际的起点是先确定支付服务商,再按照其官方集成文档完成沙箱支付。如果服务商提供托管收银台,应优先使用。只有业务确实要求用户在站内输入银行卡,并且已经明确 PCI DSS 责任和安全投入时,才考虑直接接入卡数据 API。

备注:内容仅供参考。