在企业级项目中,数据安全永远是绕不开的话题。从用户密码存储、敏感接口传输到第三方 API 对接,加密解密无处不在。本文基于一个实际生产项目 SwitchData 的加密模块,系统讲解如何在 C# 中构建一套完整的加密方案:RSA 非对称加密处理密钥交换、AES 对称加密处理数据传输、MD5 哈希实现接口签名校验。

一、加密体系概览

在开始之前,先理清三种加密原语的定位:

类型 代表算法 核心用途 密钥管理
非对称加密 RSA 密钥交换、数字签名 公钥加密、私钥解密(或反向)
对称加密 AES 大量数据加密 加解密使用同一密钥
哈希算法 MD5/SHA 完整性校验、签名摘要 不可逆,无需密钥

典型的混合加密流程:

flowchart TD A[客户端] -->|1. 生成随机 AES Key| B[服务端] A -->|2. RSA 公钥加密 AES Key| B B -->|3. RSA 私钥解密获取 AES Key| B A -->|4. AES 加密业务数据| B B -->|5. AES 解密获取明文| B

服务端使用 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();
}

关键设计要点:

  1. 分块计算:通过 GetMaxDataLength 根据 Padding 模式精确计算单块最大长度
  2. Span 切片:使用 ReadOnlySpan.Slice 避免内存拷贝
  3. MemoryStream 拼接:用 MemoryStream 累积密文块,比 List 或 byte[] 拼接更高效
  4. 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();
    }
}

核心设计:

  1. CryptoStream:将加密操作包装为流操作,自动处理块边界填充
  2. FlushFinalBlock:确保最后一个分组被正确填充和写入
  3. 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 安全加固建议

  1. 防重放攻击:API 签名加入时间戳校验(5 分钟窗口)
  2. 密钥长度:RSA 至少 2048 位,推荐 4096 位
  3. Padding 选择:优先 OAEP,兼容旧系统时使用 Pkcs1
  4. 哈希算法:MD5 已被证明存在碰撞风险,新系统推荐 SHA256+

参考项目:本文代码均来自生产级项目 SwitchData,完整实现可在以下命名空间找到:

  • SwitchData.Common.Cryptography — 加密模块(RSA/AES/Hash/KeyGen/Cache)
  • SwitchData.Business.Web.Filters — 签名校验过滤器
  • SwitchData.Business.Web.Services — 签名校验核心逻辑