## 如何加密请求中的敏感数据? --- > 请先自行阅读: > > [《微信支付开发者文档 - 平台证书:获取平台证书列表》](https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay5_1.shtml) > > [《微信支付开发者文档 - 开发指南:敏感信息加解密》](https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_3.shtml) --- ### 重要须知 请在开发过程中注意区分**商户证书**和**平台证书**: - **商户证书**与请求中的敏感数据加密无关; - **平台证书**用于加密请求中的敏感信息字段,需要通过接口获取(即 `QueryCertificatesAsync` 方法,注意证书内容需先经 AES-GCM 解密一次)。 如果你在请求过程中出现请求加密失败、服务器响应私钥解密失败的情况,请先检查是否混淆了这两个证书。 关于证书的更多注意事项,请参阅[《微信支付开发者文档 - 常见问题:证书相关》](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay7_0.shtml) --- ### 证书文件格式说明及加密示例: 需要注意的是,`QueryCertificatesAsync` 方法返回的是 CER 格式的证书文件(需先经 AES-GCM 解密一次),需区分文件格式之间的不同: - 以 `-----BEGIN PRIVATE KEY-----` 开头、 `-----END PRIVATE KEY-----` 结尾的是 **PKCS#8 私钥**文件。 - 以 `-----BEGIN PUBLIC KEY-----` 开头、 `-----END PUBLIC KEY-----` 结尾的是 **PKCS#8 公钥**文件。 - 以 `-----BEGIN CERTIFICATE--- --` 开头、 `-----END CERTIFICATE-----` 结尾的是 **CER 证书**文件,可从中导出 PKCS#8 公钥。 谨记,`QueryCertificatesAsync()` 方法返回的结果是 CER 证书,需要先通过 `RSAUtility` 工具类导出 PKCS#8 公钥,再进行数据加密;当然,`RSAUtility` 也封装了直接通过 CER 证书加密的方法。 --- ### 加密流程 对于部分接口请求传递的敏感信息,微信商户平台可能会需要使用以下方式进行加密: - 使用平台公钥/证书基于 RSA 算法加密。 开发者利用本库提供的 `RSAUtility` 工具类自行加密相关字段。下面给出一个使用 `RSAUtility` 工具类加密数据的示例代码: ```csharp string plainText = "待加密的数据"; string certificate = "CER 证书内容"; /* 通过证书加密数据 */ string cipherText = RSAUtility.EncryptWithECBByCertificate(certificate, plainText); /* 通过公钥加密数据 */ string publicKey = RSAUtility.ExportPublicKey(certificate); string cipherText = RSAUtility.EncryptWithECB(publicKey, plainText); ``` 此外,本库还封装了直接加密请求中敏感信息字段的扩展方法。下面给出一个手动调用的示例: ```csharp var request = new Models.AddProfitSharingReceiverRequest() { AppId = "AppId", Type = "PERSONAL_OPENID", Account = "OpenId", Name = "姓名明文", RelationType = "PARTNER" }; string temp = request.Name; // 此时仍是明文 client.EncryptRequestSensitiveProperty(request); string temp = request.Name; // 此时已是密文 var response = await client.ExecuteAddProfitSharingReceiverAsync(request); ``` 如果你希望本库在请求前能自动完成这项操作,你可以在构造得到 `WechatApiClient` 对象时指定自动化参数: ```csharp var options = new WechatTenpayClientOptions() { AutoEncryptRequestSensitiveProperty = true }; var client = new WechatTenpayClient(options); ``` 这样,本库会在实际发出请求前自动为你调用 `EncryptRequestSensitiveProperty()` 方法。 需要注意的是,使用该扩展方法前需先下载好平台证书,并存入全局的 `CertificateManager`。有关 `CertificateManager` 的更多介绍,请参阅下一小节。 此外,该扩展方法使用反射、并依赖 `WechatTenpaySensitivePropertyAttribute` 特性,相比较手动加密,可能会存在一定的性能开销。 --- ### 通过 `CertificateManager` 管理平台证书信息: 微信商户平台证书需要通过 API 的方式获取、且可能同时存在多个有效证书,本库提供了一个 `CertificateManager` 类型可用于管理证书信息。 你可以在构造得到 `WechatApiClient` 对象时指定证书管理器: ```csharp var manager = new InMemoryCertificateManager(); // 为便于后续使用,该对象可使用全局单例的方式声明 var options = new WechatTenpayClientOptions() { CertificateManager = certManager }; var client = new WechatTenpayClient(options); ``` > 注:`InMemoryCertificateManager` 是本库内置的基于内存实现的证书管理器;你也可自行继承并实现一个 `CertificateManager`,例如利用数据库或 Redis 等方式存取证书信息。 你应在后台周期性地调用 `QueryCertificatesAsync()` 方法,并在解密得到证书内容后,记录到证书管理器中: ```csharp /* 注意:QueryCertificatesAsync() 接口返回值需解密后再存入 */ /*    存入的证书式请参考上一小节给出的 CER 证书文件示例 */ /*    示例项目中也包含一段关于此的演示程序 */ certManager.SetEntry(new CertificateEntry("CER 证书序列号", "CER 证书内容", "证书生效时间", "证书过期时间")); ``` 当然,现在的平台证书离过期还有很久,你也可以选择“偷懒”:提前下载好平台证书,在程序启动时记录一次即可。 每个请求模型对象会包含一个名为 `WechatpayCertSerialNumber` 的公共字段,本库会根据该字段的值自动尝试在证书管理器中读取证书内容,并完成请求中敏感信息字段加密: ```csharp request.WechatpayCertSerialNumber = "平台证书序列号"; client.EncryptRequestSensitiveProperty(request); ``` 对于存在待加密敏感信息字段的请求模型对象而言,如果你不指定 `WechatpayCertSerialNumber` 字段的值,本库会自动从 `CertificateManager` 挑选一个离过期时间最远的证书。 --- ### 自定义 `CertificateManager` 实现 上一小节提到,你可自行继承并实现一个 `CertificateManager`,例如利用数据库或 Redis 等方式存取证书信息。 下面给出一个利用 Redis 存储的示例代码: ```csharp using StackExchange.Redis; using Newtonsoft.Json; public class RedisCertificateManager : CertificateManager { private const string REDIS_KEY_PREFIX = "wxpaypc-"; protected ConnectionMultiplexer Connection { get; } public RedisCertificateManager(string connectionString) { Connection = ConnectionMultiplexer.Connect(connectionString); } protected string GenerateRedisKey(string serialNumber) { return $"{REDIS_KEY_PREFIX}{serialNumber}"; } public override IEnumerable AllEntries() { RedisKey[] keys = Connection.GetServer().Keys($"{REDIS_KEY_PREFIX}*"); RedisValue[] values = Connection.GetDatabase().StringGet(keys); return values.Where(e => e.HasValue).Select(e => JsonConvert.DeserializeObject(e.ToString())); } public override void AddEntry(CertificateEntry entry) { string key = GenerateRedisKey(serialNumber); string value = JsonConvert.SerializeObject(entry); TimeSpan expiresIn = entry.ExpireTime - DateTimeOffset.Now; Connection.GetDatabase().StringSet(key, value, expiresIn); } public override CertificateEntry? GetEntry(string serialNumber) { string key = GenerateRedisKey(serialNumber); string value = Connection.GetDatabase().StringGet(GenerateRedisKey(serialNumber)); if (!string.IsNullOrEmpty(value)) { return JsonConvert.DeserializeObject(value); } return null; } public override bool RemoveEntry(string serialNumber) { string key = GenerateRedisKey(serialNumber); return Connection.GetDatabase().KeyDelete(key); } } ```