在企业级项目中,数据安全永远是绕不开的话题。从用户密码存储、敏感接口传输到第三方 API 对接,加密解密无处不在。本文基于一个实际生产项目 SwitchData 的加密模块,系统讲解如何在 C# 中构建一套完整的加密方案:RSA 非对称加密处理密钥交换、AES 对称加密处理数据传输、MD5 哈希实现接口签名校验。
一、加密体系概览
在开始之前,先理清三种加密原语的定位:
| 类型 | 代表算法 | 核心用途 | 密钥管理 |
|---|---|---|---|
| 非对称加密 | RSA | 密钥交换、数字签名 | 公钥加密、私钥解密(或反向) |
| 对称加密 | AES | 大量数据加密 | 加解密使用同一密钥 |
| 哈希算法 | MD5/SHA | 完整性校验、签名摘要 | 不可逆,无需密钥 |
典型的混合加密流程:
服务端使用 RSA 公钥(公开)加密 AES 密钥,只有服务端的 RSA 私钥能解密获取,从而实现安全的密钥交换。后续大量数据使用 AES 加密,速度远快于 RSA。
二、RSA 分块加密解密
2.1 为什么需要分块?
RSA 的加密长度不是任意的。对于 2048 位密钥,使用不同的 Padding 模式:
- Pkcs1(兼容老系统):单次可加密 keySize - 11 字节,2048 位密钥即 256 - 11 = 245 字节
- OAEP-SHA256(推荐):单次可加密 keySize - 2*HashSize - 2 字节,即 256 - 66 = 190 字节
这意味着,RSA 无法直接加密超过 245 字节的明文(2048 位密钥 + Pkcs1 填充)。当需要加密较大数据时,必须采用分块策略。
2.2 实现 RSA2Cryptographer
下面是 RSA2Cryptographer 的核心实现:
public class RSA2Cryptographer : IDisposable
{
private readonly RSA _rsa;
private static readonly RSAEncryptionPadding _defaultPadding = RSAEncryptionPadding.Pkcs1;
public static RSA2Cryptographer FromPublicKey(string publicPem)
=> new RSA2Cryptographer(ImportPublicKey(publicPem));
public static RSA2Cryptographer FromPrivateKey(string privatePem)
=> new RSA2Cryptographer(ImportPrivateKey(privatePem));
}
分块加密的核心逻辑:
private byte[] EncryptLargeData(ReadOnlySpan<byte> data, RSAEncryptionPadding padding)
{
int keySizeInBytes = _rsa.KeySize / 8;
int maxBlockSize = GetMaxDataLength(keySizeInBytes, padding);
using var ms = new MemoryStream();
for (int i = 0; i < data.Length; i += maxBlockSize)
{
int currentBlockSize = Math.Min(maxBlockSize, data.Length - i);
var block = data.Slice(i, currentBlockSize);
byte[] encryptedBlock = _rsa.Encrypt(block, padding);
ms.Write(encryptedBlock, 0, encryptedBlock.Length);
}
return ms.ToArray();
}
关键设计要点:
- 分块计算:通过 GetMaxDataLength 根据 Padding 模式精确计算单块最大长度
- Span 切片:使用 ReadOnlySpan.Slice 避免内存拷贝
- MemoryStream 拼接:用 MemoryStream 累积密文块,比 List 或 byte[] 拼接更高效
- Base64 编码:最终输出 Base64 字符串,便于 HTTP 传输
解密过程对称处理:
private byte[] DecryptLargeData(ReadOnlySpan<byte> encryptedData, RSAEncryptionPadding padding)
{
int keySizeInBytes = _rsa.KeySize / 8;
using var ms = new MemoryStream();
for (int i = 0; i < encryptedData.Length; i += keySizeInBytes)
{
int currentBlockSize = Math.Min(keySizeInBytes, encryptedData.Length - i);
var block = encryptedData.Slice(i, currentBlockSize);
try
{
byte[] decryptedBlock = _rsa.Decrypt(block, padding);
ms.Write(decryptedBlock, 0, decryptedBlock.Length);
}
catch (CryptographicException ex)
{
throw new CryptographicException("RSA 解密失败:数据块可能已损坏或者密钥不匹配", ex);
}
}
return ms.ToArray();
}
2.3 Padding 模式选择
private static int GetMaxDataLength(int keySizeInBytes, RSAEncryptionPadding padding)
{
if (padding == RSAEncryptionPadding.Pkcs1)
return keySizeInBytes - 11;
if (padding.Mode == RSAEncryptionPaddingMode.Oaep)
{
int hashSize = padding.OaepHashAlgorithm.Name switch
{
"SHA1" => 20, "SHA256" => 32,
"SHA384" => 48, "SHA512" => 64,
_ => throw new NotSupportedException("不支持的 OAEP 哈希算法")
};
return keySizeInBytes - (2 * hashSize) - 2;
}
throw new NotSupportedException("不支持的填充模式");
}
生产建议:如果是新项目,优先选择 OAEP-SHA256 模式,安全性更高;如果需要与历史系统兼容,则使用 Pkcs1。
2.4 PEM 密钥导入
PEM 是跨平台的密钥格式,RSA2Cryptographer 支持多种 PEM 格式的自动识别:
private static RSA ImportPublicKey(string pem)
{
var rsa = RSA.Create();
var keyData = ReadPem(pem);
if (TryImport(() => rsa.ImportSubjectPublicKeyInfo(keyData, out _)))
return rsa;
if (TryImport(() => rsa.ImportRSAPublicKey(keyData, out _)))
return rsa;
throw new ArgumentException("无法解析公钥");
}
private static byte[] ReadPem(string pem)
{
pem = Regex.Replace(pem, @"-----BEGIN [^-]+-----", "");
pem = Regex.Replace(pem, @"-----END [^-]+-----", "");
pem = Regex.Replace(pem, @"\s+", "");
return Convert.FromBase64String(pem);
}
支持的 PEM 格式: - 公钥:SPKI(ImportSubjectPublicKeyInfo)和 PKCS#1(ImportRSAPublicKey) - 私钥:PKCS#8(ImportPkcs8PrivateKey)和 PKCS#1(ImportRSAPrivateKey)
三、RSA 密钥管理
3.1 密钥对生成
RSAKeyPairGenerator 提供了一键生成密钥对的工具:
public static class RSAKeyPairGenerator
{
public static (string PublicKeyPem, string PrivateKeyPemPkcs8, string PrivateKeyPemPkcs1) GenerateKeyPair(int keySize = 2048)
{
using RSA rsa = RSA.Create(keySize);
var pub = rsa.ExportSubjectPublicKeyInfo();
string publicPem = BuildPem("PUBLIC KEY", pub);
var pri8 = rsa.ExportPkcs8PrivateKey();
string privatePemPkcs8 = BuildPem("PRIVATE KEY", pri8);
var pri1 = rsa.ExportRSAPrivateKey();
string privatePemPkcs1 = BuildPem("RSA PRIVATE KEY", pri1);
return (publicPem, privatePemPkcs8, privatePemPkcs1);
}
private static string BuildPem(string title, byte[] data)
{
var b64 = Convert.ToBase64String(data);
var sb = new StringBuilder();
sb.AppendLine("-----BEGIN " + title + "-----");
for (int i = 0; i < b64.Length; i += 64)
sb.AppendLine(b64.Substring(i, Math.Min(64, b64.Length - i)));
sb.AppendLine("-----END " + title + "-----");
return sb.ToString();
}
}
3.2 密钥缓存
RSA 实例创建成本较高,RSA2CryptographerCache 使用 ConcurrentDictionary 实现线程安全的缓存:
public static class RSA2CryptographerCache
{
private static readonly ConcurrentDictionary<string, RSA2Cryptographer> PublicCache = new();
private static readonly ConcurrentDictionary<string, RSA2Cryptographer> PrivateCache = new();
public static RSA2Cryptographer GetPublic(string pem)
{
pem = RSA2Cryptographer.NormalizePem(pem);
return PublicCache.GetOrAdd(pem, _ => RSA2Cryptographer.FromPublicKey(pem));
}
public static RSA2Cryptographer GetPrivate(string pem)
{
pem = RSA2Cryptographer.NormalizePem(pem);
var rsa2 = PrivateCache.GetOrAdd(pem, _ => RSA2Cryptographer.FromPrivateKey(pem));
return rsa2.Clone();
}
}
为什么私钥要 Clone?RSA 对象在执行 Encrypt/Decrypt 等操作时是非线程安全的。缓存的私钥实例被克隆后,每次使用都是独立实例,天然支持并发调用。
四、AES 对称加密
4.1 使用 CryptoStream 实现
SymmetricCryptographer 封装了 AES 对称加密:
public sealed class SymmetricCryptographer
{
private static readonly byte[] DefaultKey = [
0xBC, 0xA7, 0xBC, 0x2E, 0x4E, 0xA1, 0xB9, 0xA7,
0x6F, 0x44, 0x4B, 0xD7, 0x58, 0xC5, 0x22, 0x61,
0xB1, 0xE5, 0x29, 0x04, 0x0E, 0xB0, 0x47, 0x7F,
0xF2, 0xDD, 0xA8, 0x55, 0xD7, 0x7D, 0x68, 0x8A ];
private static readonly byte[] DefaultIV = [
0x75, 0xEB, 0x6A, 0x6A, 0xDB, 0xD1, 0xCB, 0x86,
0x02, 0x9E, 0xC0, 0x43, 0xF5, 0x63, 0xEF, 0xC4 ];
public SymmetricAlgorithm Algorithm { get; }
public SymmetricCryptographer(SymmetricAlgorithm algorithm, byte[] key, byte[] iv)
{
Algorithm = algorithm;
Algorithm.Key = key;
Algorithm.IV = iv;
}
public byte[] Encrypt(byte[] plaintext)
{
using ICryptoTransform transform = Algorithm.CreateEncryptor();
return Transform(transform, plaintext);
}
public byte[] Decrypt(byte[] encryptedText)
{
using ICryptoTransform transform = Algorithm.CreateDecryptor();
return Transform(transform, encryptedText);
}
private static byte[] Transform(ICryptoTransform transform, byte[] buffer)
{
using var ms = new MemoryStream();
using var cs = new CryptoStream(ms, transform, CryptoStreamMode.Write);
cs.Write(buffer, 0, buffer.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
}
核心设计:
- CryptoStream:将加密操作包装为流操作,自动处理块边界填充
- FlushFinalBlock:确保最后一个分组被正确填充和写入
- using 释放:CryptoStream 和 MemoryStream 都在使用后释放
4.2 便捷静态方法
public static string Encrypt(string plaintext)
{
byte[] input = Encoding.UTF8.GetBytes(plaintext);
using var aes = Aes.Create();
var sc = new SymmetricCryptographer(aes, DefaultKey, DefaultIV);
byte[] output = sc.Encrypt(input);
return Convert.ToBase64String(output);
}
public static string Decrypt(string encryptedText)
{
byte[] input = Convert.FromBase64String(encryptedText);
using var aes = Aes.Create();
var sc = new SymmetricCryptographer(aes, DefaultKey, DefaultIV);
byte[] output = sc.Decrypt(input);
return Encoding.UTF8.GetString(output);
}
生产注意:硬编码 Key/IV 仅适合内部系统。对于公网 API,必须动态生成 Key 并通过 RSA 安全交换。
五、哈希算法
HashCryptographer 提供 MD5/SHA 哈希计算:
public sealed class HashCryptographer
{
public static string ComputeHash(string plaintext, bool removeSplitChar = false, HashAlgorithm hashAlgorithm = null)
{
var hashBytes = ComputeHash(Encoding.UTF8.GetBytes(plaintext), hashAlgorithm);
return removeSplitChar ? Convert.ToHexString(hashBytes) : BitConverter.ToString(hashBytes);
}
}
默认使用 MD5,也可传入 SHA256.Create() 等实例切换算法。
六、实战:API 签名校验
将上述三种加密原语整合,构建一个完整的 API 签名校验方案。
6.1 签名规则
客户端签名生成流程:
签名串 = userId + "," + timestamp + "," + secretKey
签名 = MD5(签名串).ToUpper()
HTTP 头部携带三个字段: - user-id:API 用户标识 - timestamp:请求时间戳(5 分钟有效期) - sign:MD5 签名
6.2 签名校验实现
public static class ApiSignValidator
{
private const int TimeSpanMinutes = 5;
public static async Task<JsonDataResult> Validate(HttpContext context)
{
string userId = context.Request.Headers["user-id"];
string timeStamp = context.Request.Headers["timestamp"];
string sign = context.Request.Headers["sign"];
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(timeStamp) || string.IsNullOrEmpty(sign))
throw new Exception("缺少必要的签名头");
string secret = await ApiUserService.GetSecretAsync(userId);
if (string.IsNullOrEmpty(secret))
throw new UnauthorizedAccessException("用户不存在或已禁用");
DateTime time;
if (timeStamp.IsNumber())
time = TimeExtensions.FromUnixTimeMilliseconds(long.Parse(timeStamp));
else
time = DateTime.Parse(timeStamp);
if (Math.Abs((DateTime.Now - time).TotalMinutes) > TimeSpanMinutes)
throw new Exception("请求已过期");
string signString = string.Join(",", userId, timeStamp, secret);
string validSign = HashCryptographer.ComputeHash(signString, true);
if (!string.Equals(sign, validSign, StringComparison.OrdinalIgnoreCase))
throw new Exception("签名不匹配");
return new JsonDataResult { message = "认证成功" };
}
}
6.3 ASP.NET Core Filter 集成
public class ApiSignAuthorizationFilter : IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (IsAllowAnonymous(context))
return;
var result = await ApiSignValidator.Validate(context.HttpContext);
if (!result.success)
context.Result = new JsonResult(result) { StatusCode = StatusCodes.Status401Unauthorized };
}
private static bool IsAllowAnonymous(AuthorizationFilterContext context)
{
var endpoint = context.HttpContext.GetEndpoint();
return endpoint?.Metadata.GetMetadata<AllowAnonymousAttribute>() != null;
}
}
七、RSA 数字签名
// 签名(私钥)
public string Sign(string content)
{
var data = _encoding.GetBytes(content);
var signatureBytes = _rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signatureBytes);
}
// 验签(公钥)
public bool Verify(string content, string signatureBase64)
{
var data = _encoding.GetBytes(content);
var signatureBytes = Convert.FromBase64String(signatureBase64);
return _rsa.VerifyData(data, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
八、最佳实践与总结
8.1 密钥管理
| 做法 | 说明 |
|---|---|
| 生产环境密钥不要硬编码 | 应使用密钥管理服务(KMS)或环境变量 |
| 密钥定期轮换 | 设置有效期,过期自动更新 |
| 最小权限原则 | 公钥可公开,私钥严格保密 |
8.2 加密选型
- 密钥交换:RSA 2048 位 + OAEP-SHA256 填充,安全且兼容性好
- 数据加密:AES-256-CBC(或 GCM 模式),速度快、安全性高
- 完整性校验:MD5 适合快速签名校验,SHA256 适合高安全场景
8.3 性能考量
- RSA 实例创建成本高,使用 ConcurrentDictionary 缓存
- 私钥使用时 Clone 避免并发冲突
- 大数据加密分块处理,使用 Span 减少内存拷贝
- CryptoStream 流式加密避免数据堆积内存
8.4 安全加固建议
- 防重放攻击:API 签名加入时间戳校验(5 分钟窗口)
- 密钥长度:RSA 至少 2048 位,推荐 4096 位
- Padding 选择:优先 OAEP,兼容旧系统时使用 Pkcs1
- 哈希算法:MD5 已被证明存在碰撞风险,新系统推荐 SHA256+
参考项目:本文代码均来自生产级项目 SwitchData,完整实现可在以下命名空间找到:
- SwitchData.Common.Cryptography — 加密模块(RSA/AES/Hash/KeyGen/Cache)
- SwitchData.Business.Web.Filters — 签名校验过滤器
- SwitchData.Business.Web.Services — 签名校验核心逻辑