PAYATHON 2026

如何使用 .crt 和 .key 文件发送 HTTPS POST 请求

支付老李

结论

.crt 通常只包含公钥证书,.key 则包含私钥。旧版 .NET Core 和 .NET Framework 无法像 cURL 一样,直接将两个 PEM 文件自动组合成客户端证书。

对于 nopCommerce 4.0 这类较旧的运行环境,建议先把 .crt 和 .key 合并为 PKCS#12 文件,也就是 .pfx 或 .p12,然后通过 X509Certificate2 加载,再将证书添加到 HttpClientHandler.ClientCertificates 中发送 HTTPS POST 请求。

添加到请求中的证书必须包含私钥,也就是 certificate.HasPrivateKey 必须为 true。

生成包含私钥的 PFX 文件

如果 .crt 和 .key 使用 PEM 格式,可以通过 OpenSSL 将它们合并:

openssl pkcs12 -export \
  -out createorder.pfx \
  -inkey createorder.key \
  -in createorder.crt

执行命令时,OpenSSL 会要求设置导出密码。生产环境中建议使用非空密码。

如果支付服务商还提供了中间证书链,也可以一起加入:

openssl pkcs12 -export \
  -out createorder.pfx \
  -inkey createorder.key \
  -in createorder.crt \
  -certfile intermediate-ca.crt

如果 OpenSSL 提示无法加载私钥,先检查证书和私钥的文件格式:

openssl x509 -in createorder.crt -noout -subject -issuer
openssl pkey -in createorder.key -check

还要确认私钥与证书匹配。对于 RSA 证书,可以比较两者的 modulus:

openssl x509 -noout -modulus -in createorder.crt | openssl sha256
openssl rsa -noout -modulus -in createorder.key | openssl sha256

两条命令的输出应该相同。

使用 HttpClient 发送请求

下面的写法适用于 nopCommerce 4.0 所在的旧版 .NET 运行环境,不依赖 ImportFromPem 等较新的 API:

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

public async Task<string> RequestTokenAsync()
{
    var pluginPath = CommonHelper.MapPath(
        "~/Plugins/Payments.CityBankApi/"
    );

    var certificatePath = Path.Combine(
        pluginPath,
        "createorder.pfx"
    );

    const string certificatePassword = "your-pfx-password";
    const string requestUrl =
        "https://sandbox.thecitybank.com:7788/transaction/token";

    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

    var certificate = new X509Certificate2(
        certificatePath,
        certificatePassword,
        X509KeyStorageFlags.MachineKeySet |
        X509KeyStorageFlags.PersistKeySet
    );

    if (!certificate.HasPrivateKey)
    {
        certificate.Dispose();

        throw new InvalidOperationException(
            "客户端证书没有关联私钥,请确认 PFX 是由正确的 CRT 和 KEY 文件生成的。"
        );
    }

    using (certificate)
    using (var handler = new HttpClientHandler())
    {
        handler.ClientCertificateOptions =
            ClientCertificateOption.Manual;

        handler.ClientCertificates.Add(certificate);

        using (var client = new HttpClient(handler))
        {
            var json =
                "{\"userName\":\"test\",\"password\":\"123456Aa\"}";

            using (var content = new StringContent(
                json,
                Encoding.UTF8,
                "application/json"))
            using (var response = await client.PostAsync(
                requestUrl,
                content))
            {
                var responseBody =
                    await response.Content.ReadAsStringAsync();

                if (!response.IsSuccessStatusCode)
                {
                    throw new HttpRequestException(
                        "支付接口返回错误。HTTP 状态码:" +
                        (int)response.StatusCode +
                        ",响应内容:" +
                        responseBody
                    );
                }

                return responseBody;
            }
        }
    }
}

这段代码与 PHP cURL 的主要配置对应如下:

PHP cURL 配置C# 对应处理
CURLOPT_SSLCERTPFX 中的客户端证书
CURLOPT_SSLKEYPFX 中关联的私钥
CURLOPT_POSTFIELDSStringContent
Content-Type: application/jsonStringContent 的媒体类型
curl_exec()HttpClient.PostAsync()

如果插件需要频繁发送请求,不适合每次都创建新的 HttpClient。可以缓存已经配置客户端证书的 HttpClient,也可以通过 nopCommerce 的依赖注入机制注册并复用。无论采用哪种方式,都要保证在 HttpClient 的生命周期内,证书不会被提前释放。

为什么直接读取 .crt 和 .key 不够

下面的代码只是读取文件中的文本:

string certificateText = File.ReadAllText(certPath);
string privateKeyText = File.ReadAllText(keyPath);

它不会自动创建同时包含证书和私钥的 X509Certificate2。即使 .crt 解析成功,得到的证书通常仍然是:

certificate.HasPrivateKey == false

双向 TLS(mTLS)握手要求客户端使用私钥进行签名。如果添加到 ClientCertificates 中的只有公钥证书,服务器就无法完成客户端身份验证。

较新的 .NET 可以通过 X509Certificate2.CreateFromPemFile() 或 CopyWithPrivateKey() 在运行时组合 PEM 文件,但 nopCommerce 4.0 常用的旧目标框架不支持这些 API。为了兼容这类环境,最好预先生成 PFX。如果业务必须在运行时直接解析 .crt 和 .key,则需要引入兼容目标框架的密码学库,例如 BouncyCastle,并妥善处理 RSA、EC、PKCS#1、PKCS#8 和加密私钥之间的差异。

不要关闭服务器证书验证

PHP 示例中包含以下配置:

CURLOPT_SSL_VERIFYPEER = false
CURLOPT_SSL_VERIFYHOST = false

这些配置会关闭服务器证书和主机名验证,带来中间人攻击风险,不应直接照搬到 C# 代码中。HttpClientHandler 默认会验证服务器证书,应保留这一设置。

如果沙箱服务器使用私有 CA 或未受信任的证书,应该将相应的根证书或中间证书安装到运行服务器的受信任证书存储中,而不是无条件接受所有服务器证书。

其他注意事项

  • 不要启用 Ssl3。该协议已经不安全,许多服务器和现代系统也已将其禁用。
  • 除非支付服务商明确要求使用其他协议,否则优先选择 SecurityProtocolType.Tls12。
  • PFX 密码、接口用户名和密码应保存在安全配置或密钥管理系统中,不要硬编码到插件源码里。
  • IIS 应用程序池账户必须有权读取 PFX 文件及其私钥。如果出现 “Keyset does not exist” 或 “Access denied”,通常需要检查应用程序池身份和私钥存储权限。
  • 如果当前托管环境没有权限使用 MachineKeySet | PersistKeySet,可以尝试 X509KeyStorageFlags.DefaultKeySet。具体行为取决于操作系统、IIS 身份和 .NET 运行时。
  • 不要通过 throw ex; 重新抛出异常,因为这样会破坏原始堆栈信息。应直接写成:
catch
{
    throw;
}
  • 如果 TLS 握手仍然失败,应检查 PFX 是否包含私钥、证书是否过期、证书用途是否允许客户端身份验证、证书链是否完整,以及服务器是否认可该客户端证书。

备注:内容仅供参考。