2021-11-25 21:15:54 +08:00
|
|
|
|
## 如何加密请求中的敏感数据?
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
> 请先自行阅读:
|
|
|
|
|
>
|
|
|
|
|
> [《微信支付开发者文档 - 平台证书:获取平台证书列表》](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)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### 重要须知
|
|
|
|
|
|
|
|
|
|
请在开发过程中注意区分**商户证书**和**平台证书**:
|
|
|
|
|
|
|
|
|
|
- **商户证书**与请求中的敏感数据加密无关;
|
2022-03-03 17:19:29 +08:00
|
|
|
|
- **平台证书**用于加密请求中的敏感信息字段,需要在程序运行时实时通过接口动态获取(即 `QueryCertificatesAsync` 方法,注意证书内容需先经 AES-GCM 解密一次)。
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
2022-03-03 17:19:29 +08:00
|
|
|
|
如果你在开发过程中出现请求加密失败、服务器响应私钥解密失败的情况,请先检查是否混淆了这两个证书。
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
关于证书的更多注意事项,请参阅[《微信支付开发者文档 - 常见问题:证书相关》](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 公钥**文件。
|
|
|
|
|
|
2021-11-26 02:46:04 +08:00
|
|
|
|
- 以 `-----BEGIN CERTIFICATE--- --` 开头、 `-----END CERTIFICATE-----` 结尾的是 **CRT/CER 证书**文件,可从中导出 PKCS#8 公钥和证书序列号。
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
2021-11-26 02:46:04 +08:00
|
|
|
|
谨记,`QueryCertificatesAsync()` 方法返回的结果是 CRT/CER 证书,需要先通过 `RSAUtility` 工具类导出 PKCS#8 公钥,再进行数据加密;当然,`RSAUtility` 也封装了直接通过 CRT/CER 证书加密的方法。
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### 加密流程
|
|
|
|
|
|
|
|
|
|
对于部分接口请求传递的敏感信息,微信商户平台可能会需要使用以下方式进行加密:
|
|
|
|
|
|
2021-11-25 21:17:44 +08:00
|
|
|
|
- 使用平台公钥/证书基于 RSA 算法加密。
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
开发者利用本库提供的 `RSAUtility` 工具类自行加密相关字段。下面给出一个使用 `RSAUtility` 工具类加密数据的示例代码:
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
string plainText = "待加密的数据";
|
2021-11-26 02:46:04 +08:00
|
|
|
|
string certificate = "CRT/CER 证书内容";
|
2021-11-25 21:15:54 +08:00
|
|
|
|
/* 通过证书加密数据 */
|
|
|
|
|
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"
|
|
|
|
|
};
|
|
|
|
|
|
2022-05-09 20:31:03 +08:00
|
|
|
|
Console.WriteLine("before: {0}", request.Name); // 此时仍是明文
|
2021-11-25 21:15:54 +08:00
|
|
|
|
client.EncryptRequestSensitiveProperty(request);
|
2022-05-09 20:31:03 +08:00
|
|
|
|
Console.WriteLine("after: {0}", request.Name); // 此时已是密文
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
var response = await client.ExecuteAddProfitSharingReceiverAsync(request);
|
|
|
|
|
```
|
|
|
|
|
|
2022-05-09 20:31:03 +08:00
|
|
|
|
如果你希望本库在请求前能自动完成这项操作,你可以在构造得到 `WechatTenpayClient` 对象时指定自动化参数:
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
```csharp
|
2022-05-06 20:29:27 +08:00
|
|
|
|
var options = new WechatTenpayClientOptions()
|
|
|
|
|
{
|
2022-01-11 17:52:01 +08:00
|
|
|
|
// 其他配置项略
|
2022-05-06 20:29:27 +08:00
|
|
|
|
AutoEncryptRequestSensitiveProperty = true
|
2022-01-11 17:52:01 +08:00
|
|
|
|
};
|
2021-11-25 21:15:54 +08:00
|
|
|
|
var client = new WechatTenpayClient(options);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这样,本库会在实际发出请求前自动为你调用 `EncryptRequestSensitiveProperty()` 方法。
|
|
|
|
|
|
|
|
|
|
需要注意的是,使用该扩展方法前需先下载好平台证书,并存入全局的 `CertificateManager`。有关 `CertificateManager` 的更多介绍,请参阅下一小节。
|
|
|
|
|
|
2021-12-03 16:55:29 +08:00
|
|
|
|
此外,该扩展方法使用反射、并依赖 `WechatTenpaySensitiveAttribute`、`WechatTenpaySensitivePropertyAttribute` 特性,相比较手动加密,可能会存在一定的性能开销。
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### 通过 `CertificateManager` 管理平台证书信息:
|
|
|
|
|
|
|
|
|
|
微信商户平台证书需要通过 API 的方式获取、且可能同时存在多个有效证书,本库提供了一个 `CertificateManager` 类型可用于管理证书信息。
|
|
|
|
|
|
2022-05-09 20:31:03 +08:00
|
|
|
|
你可以在构造得到 `WechatTenpayClient` 对象时指定证书管理器:
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
```csharp
|
2021-12-24 20:54:26 +08:00
|
|
|
|
var manager = new InMemoryCertificateManager(); // 为便于后续使用,该对象可使用同一商户号下全局单例的方式声明
|
2022-05-06 20:29:27 +08:00
|
|
|
|
var options = new WechatTenpayClientOptions()
|
|
|
|
|
{
|
2022-01-11 17:52:01 +08:00
|
|
|
|
// 其他配置项略
|
2022-05-06 20:29:27 +08:00
|
|
|
|
PlatformCertificateManager = manager
|
2022-01-11 17:52:01 +08:00
|
|
|
|
};
|
2021-11-25 21:15:54 +08:00
|
|
|
|
var client = new WechatTenpayClient(options);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> 注:`InMemoryCertificateManager` 是本库内置的基于内存实现的证书管理器;你也可自行继承并实现一个 `CertificateManager`,例如利用数据库或 Redis 等方式存取证书信息。
|
|
|
|
|
|
|
|
|
|
你应在后台周期性地调用 `QueryCertificatesAsync()` 方法,并在解密得到证书内容后,记录到证书管理器中:
|
|
|
|
|
|
|
|
|
|
```csharp
|
2022-03-12 13:06:58 +08:00
|
|
|
|
/* 注意:QueryCertificatesAsync() 接口返回值需解密后再存入 */
|
|
|
|
|
/* 存入的证书格式请参考上一小节给出的 CRT/CER 证书文件示例 */
|
|
|
|
|
/* 示例项目中也包含一段关于此的演示程序 */
|
2021-11-29 12:14:42 +08:00
|
|
|
|
manager.SetEntry(new CertificateEntry("CRT/CER 证书序列号", "CRT/CER 证书内容", "证书生效时间", "证书过期时间"));
|
2021-11-25 21:15:54 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
当然,现在的平台证书离过期还有很久,你也可以选择“偷懒”:提前下载好平台证书,在程序启动时记录一次即可。
|
|
|
|
|
|
2022-01-21 14:51:30 +08:00
|
|
|
|
每个请求模型对象会包含一个名为 `WechatpayCertificateSerialNumber` 的公共字段,本库会根据该字段的值自动尝试在证书管理器中读取证书内容,并完成请求中敏感信息字段加密:
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
```csharp
|
2022-01-21 14:51:30 +08:00
|
|
|
|
request.WechatpayCertificateSerialNumber = "平台证书序列号";
|
2021-11-25 21:15:54 +08:00
|
|
|
|
client.EncryptRequestSensitiveProperty(request);
|
|
|
|
|
```
|
|
|
|
|
|
2022-01-21 14:51:30 +08:00
|
|
|
|
对于存在待加密敏感信息字段的请求模型对象而言,如果你不指定 `WechatpayCertificateSerialNumber` 字段的值,本库会自动从 `CertificateManager` 挑选一个离过期时间最远的证书。
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### 自定义 `CertificateManager` 实现
|
|
|
|
|
|
|
|
|
|
上一小节提到,你可自行继承并实现一个 `CertificateManager`,例如利用数据库或 Redis 等方式存取证书信息。
|
|
|
|
|
|
2021-12-01 22:06:50 +08:00
|
|
|
|
下面给出一个利用 Redis 的 HASH 数据结构存储的示例代码:
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
using StackExchange.Redis;
|
|
|
|
|
|
|
|
|
|
public class RedisCertificateManager : CertificateManager
|
|
|
|
|
{
|
2021-11-25 21:45:34 +08:00
|
|
|
|
private const string REDIS_KEY_PREFIX = "wxpaypc-";
|
|
|
|
|
|
2021-12-01 22:06:50 +08:00
|
|
|
|
private readonly ConnectionMultiplexer Connection { get; }
|
2021-11-25 21:15:54 +08:00
|
|
|
|
|
|
|
|
|
public RedisCertificateManager(string connectionString)
|
|
|
|
|
{
|
2021-11-25 21:45:34 +08:00
|
|
|
|
Connection = ConnectionMultiplexer.Connect(connectionString);
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-01 22:06:50 +08:00
|
|
|
|
private string GenerateRedisKey(string serialNumber)
|
2021-11-25 21:45:34 +08:00
|
|
|
|
{
|
|
|
|
|
return $"{REDIS_KEY_PREFIX}{serialNumber}";
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-01 22:06:50 +08:00
|
|
|
|
private CertificateEntry ConvertHashEntriesToCertificateEntry(HashEntry[] values)
|
|
|
|
|
{
|
|
|
|
|
if (values == null) throw new ArgumentNullException(nameof(values));
|
|
|
|
|
|
|
|
|
|
IDictionary<string, string> map = values.ToDictionary(k => k.Name.ToString(), v => v.Value.ToString());
|
|
|
|
|
return new CertificateEntry(
|
|
|
|
|
serialNumber: map[nameof(CertificateEntry.SerialNumber)],
|
|
|
|
|
certificate: map[nameof(CertificateEntry.Certificate)],
|
|
|
|
|
effectiveTime: DateTimeOffset.Parse(map[nameof(CertificateEntry.EffectiveTime)]),
|
|
|
|
|
expireTime: DateTimeOffset.Parse(map[nameof(CertificateEntry.ExpireTime)])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private HashEntry[] ConvertCertificateEntryToHashEntries(CertificateEntry entry)
|
|
|
|
|
{
|
|
|
|
|
return new HashEntry[]
|
|
|
|
|
{
|
|
|
|
|
new HashEntry(nameof(CertificateEntry.SerialNumber), entry.SerialNumber),
|
|
|
|
|
new HashEntry(nameof(CertificateEntry.Certificate), entry.Certificate),
|
|
|
|
|
new HashEntry(nameof(CertificateEntry.EffectiveTime), entry.EffectiveTime.ToString()),
|
|
|
|
|
new HashEntry(nameof(CertificateEntry.ExpireTime), entry.ExpireTime.ToString())
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-25 21:45:34 +08:00
|
|
|
|
public override IEnumerable<CertificateEntry> AllEntries()
|
|
|
|
|
{
|
2021-11-25 21:47:06 +08:00
|
|
|
|
// 生产环境中不应该使用 Redis KEYS 命令,这里代码仅作参考
|
2021-12-01 22:06:50 +08:00
|
|
|
|
// 你可以使用 SCAN + CURSOR 来实现类似功能
|
2021-11-25 21:45:34 +08:00
|
|
|
|
RedisKey[] keys = Connection.GetServer().Keys($"{REDIS_KEY_PREFIX}*");
|
2021-12-01 22:06:50 +08:00
|
|
|
|
if (keys.Any())
|
|
|
|
|
{
|
|
|
|
|
Task[] pipelineTasks = keys.Select(key => Connection.GetDatabase().HashGetAllAsync(key)).ToArray();
|
|
|
|
|
Connection.WaitAll(pipelineTasks);
|
|
|
|
|
|
|
|
|
|
return pipelineTasks
|
|
|
|
|
.Where(t => t.IsCompletedSuccessfully && t.Result.Any())
|
|
|
|
|
.Select(t => ConvertHashEntriesToCertificateEntry(t.Result))
|
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.Empty<CertificateEntry>();
|
2021-11-25 21:15:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-11-25 21:45:34 +08:00
|
|
|
|
public override void AddEntry(CertificateEntry entry)
|
2021-11-25 21:15:54 +08:00
|
|
|
|
{
|
2021-11-25 21:45:34 +08:00
|
|
|
|
string key = GenerateRedisKey(serialNumber);
|
2021-12-01 22:06:50 +08:00
|
|
|
|
HashEntry[] values = ConvertCertificateEntryToHashEntries(entry);
|
|
|
|
|
Connection.GetDatabase().HashSet(key, values);
|
|
|
|
|
Connection.GetDatabase().KeyExpire(key, entry.ExpireTime - DateTimeOffset.Now);
|
2021-11-25 21:15:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-11-25 21:45:34 +08:00
|
|
|
|
public override CertificateEntry? GetEntry(string serialNumber)
|
2021-11-25 21:15:54 +08:00
|
|
|
|
{
|
2021-11-25 21:45:34 +08:00
|
|
|
|
string key = GenerateRedisKey(serialNumber);
|
2021-12-01 22:06:50 +08:00
|
|
|
|
HashEntry[] values = Connection.GetDatabase().HashGetAll(key);
|
|
|
|
|
if (values.Any())
|
2021-11-25 21:45:34 +08:00
|
|
|
|
{
|
2021-12-01 22:06:50 +08:00
|
|
|
|
return ConvertHashEntriesToCertificateEntry(values);
|
2021-11-25 21:45:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2021-11-25 21:15:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-11-25 21:45:34 +08:00
|
|
|
|
public override bool RemoveEntry(string serialNumber)
|
2021-11-25 21:15:54 +08:00
|
|
|
|
{
|
2021-11-25 21:45:34 +08:00
|
|
|
|
string key = GenerateRedisKey(serialNumber);
|
|
|
|
|
return Connection.GetDatabase().KeyDelete(key);
|
2021-11-25 21:15:54 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-05-06 20:29:27 +08:00
|
|
|
|
```
|