
1. 项目概述与核心价值最近在做一个需要处理用户敏感数据的项目比如配置文件里的数据库连接字符串、API传输的身份令牌这些信息明文存储或传输简直就是“裸奔”。安全这事儿不能含糊。我第一时间就想到了AES毕竟它是目前公认安全且高效的对称加密算法标准。但在具体实现时我发现网上很多C#的AES示例要么过于简单只演示一个Encrypt方法要么混杂了各种模式让人眼花缭乱真正能直接拿来用到生产环境、考虑周全的工具类并不多。所以我决定自己动手封装一个专注于AES-CBC模式的C#工具类。这个工具类的目标很明确开箱即用、安全可靠、功能完整。它不仅仅是一两个加密解密方法而是包含了密钥管理、异常处理、编码转换等一套完整的解决方案并且附带了可以直接复制粘贴的完整源码和详细的使用教程。无论你是刚接触C#加密的新手还是需要在项目中快速集成安全模块的老手这个工具类都能让你省去大量查阅资料和踩坑的时间。AES-CBCCipher Block Chaining密码分组链接模式之所以成为我的首选是因为它在安全性和实用性之间取得了很好的平衡。相比ECB模式CBC通过引入初始化向量IV使得即使相同的明文、相同的密钥每次加密也会产生不同的密文有效抵御了模式分析攻击。虽然现在有GCM这种带认证的模式更受推崇但在许多对内网通信、本地数据加密等不需要完整性校验或校验通过其他方式实现的场景下CBC因其广泛的库支持和清晰的流程依然是许多企业和项目的务实选择。2. AES-CBC核心原理与C#实现选型在动手写代码之前我们必须先搞清楚AES-CBC到底是怎么工作的以及为什么在C#里要这么实现。知其然更要知其所以然这样遇到问题才知道从哪里排查。2.1 AES-CBC加密解密流程拆解想象一下你要加密一本日记。AES算法本身规定了一次只能加密“一页”一个16字节的数据块。如果你的日记很长就需要一个规则来决定如何加密“下一页”。CBC模式就是这个规则。加密过程核心思想链式混淆准备首先你需要一个密钥Key和一个初始化向量IV。IV可以理解为一本“随机密码本”的第一页它必须是随机的且每次加密都应不同。第一块将你的日记“第一页”明文与IV进行异或XOR操作。这相当于用IV对第一页内容做了一次“混淆”。加密将混淆后的结果用AES算法和你的密钥进行加密得到第一页的密文。同时这第一页的密文将作为“混淆码”用于下一页。后续块将日记“第二页”明文与上一页的密文进行异或混淆然后再用AES加密。如此循环直到最后一页。这就形成了一条“链”每一块的加密都依赖于前一块的结果。解密过程逆向操作第一块用密钥解密第一页密文得到“混淆后的中间数据”。还原将这个中间数据与IV进行异或就得到了第一页的原始明文。后续块用密钥解密第二页密文得到中间数据然后与第一页的密文进行异或得到第二页明文。以此类推。关键点IV必须随机且唯一这是CBC安全的基础。如果IV固定或可预测攻击者可能发起重放攻击或部分破解。在C#中我们通常使用RNGCryptoServiceProvider或RandomNumberGenerator来生成密码学安全的随机IV。IV不需要保密但需与密文一起传输/存储因为解密时需要同样的IV。通常的做法是将IV预置在密文字节数组的前面例如前16字节。填充PaddingAES是块加密要求输入数据长度必须是16字节的倍数。但我们的数据长度通常是任意的。因此需要在加密前对明文进行填充解密后再去除填充。C#的Aes类默认使用PKCS7填充模式它会自动处理这个问题。2.2 C#中System.Security.Cryptography命名空间C#为我们提供了强大且易用的加密库System.Security.Cryptography。对于AES核心类是Aes或AesManaged但推荐使用Aes.Create()工厂方法它能在不同平台选择最佳实现。在我们的工具类设计中将围绕Aes类实例展开。主要配置以下几个属性KeySize: 密钥长度可选128, 192, 256位。256位安全性最高是当前推荐标准。Mode: 加密模式设置为CipherMode.CBC。Padding: 填充模式设置为PaddingMode.PKCS7默认。Key: 加密解密使用的密钥字节数组。IV: 初始化向量字节数组。注意密钥管理是命门。绝对不要将密钥硬编码在源码中密钥应该通过安全的渠道分发和存储例如从环境变量、经过加密的配置文件或硬件安全模块HSM中读取。工具类只负责“如何使用密钥”不负责“密钥从哪来”。2.3 工具类的设计目标与接口规划基于以上原理我们的工具类AesCbcHelper需要提供清晰、安全的接口加密输入明文字符串或字节数组、密钥字节数组输出包含IV的完整密文Base64字符串便于传输和存储。解密输入密文Base64字符串、密钥字节数组输出原始明文。辅助方法生成随机密钥、从密码派生密钥使用PBKDF2增强安全性等。异常处理对密钥长度错误、密文格式错误、填充错误等情况进行友好封装抛出明确的异常。这样用户只需要关心“我要加密什么”和“我的密钥是什么”剩下的复杂流程都由工具类默默完成。3. 完整源码实现与逐行解析下面就是AesCbcHelper工具类的完整实现。我会在关键代码后加上详细注释解释每一行代码的意图和背后的考量。using System; using System.IO; using System.Security.Cryptography; using System.Text; namespace YourProject.Security { /// summary /// AES-CBC 对称加密解密工具类 /// 特点自动处理IV生成与拼接使用PKCS7填充输出/输入为Base64字符串 /// /summary public static class AesCbcHelper { // AES块大小固定为16字节128位 private const int BLOCK_SIZE_BYTES 16; /// summary /// 使用AES-CBC模式加密字符串 /// /summary /// param nameplainText待加密的明文/param /// param namekey密钥字节数组必须为16, 24或32字节长度对应AES-128, AES-192, AES-256/param /// returnsBase64编码的密文字符串其中前16字节为随机生成的IV/returns /// exception crefArgumentNullException明文或密钥为空/exception /// exception crefCryptographicException密钥长度无效或加密过程出错/exception public static string EncryptString(string plainText, byte[] key) { if (string.IsNullOrEmpty(plainText)) throw new ArgumentNullException(nameof(plainText)); if (key null || key.Length 0) throw new ArgumentNullException(nameof(key)); // 验证密钥长度 if (!IsValidKeySize(key.Length * 8)) // 参数是比特长度 throw new ArgumentException($无效的密钥长度: {key.Length} 字节。支持的密钥长度为16(AES-128), 24(AES-192), 32(AES-256)字节。, nameof(key)); byte[] encryptedBytes; // 使用using语句确保Aes对象和CryptoStream被正确释放即使发生异常 using (Aes aesAlg Aes.Create()) { // 配置AES算法参数 aesAlg.Key key; // 设置用户提供的密钥 aesAlg.Mode CipherMode.CBC; // 设置为CBC模式 aesAlg.Padding PaddingMode.PKCS7; // 明确指定PKCS7填充默认即是显式声明更清晰 // 注意这里没有设置IVAes.Create()会自动生成一个随机的、密码学安全的IV。 // 创建加密器 ICryptoTransform encryptor aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); // 准备内存流来保存加密后的数据 using (MemoryStream msEncrypt new MemoryStream()) { // 首先将IV写入内存流的开头。解密时需要相同的IV。 msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length); // 使用CryptoStream将明文流加密后写入内存流 using (CryptoStream csEncrypt new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt new StreamWriter(csEncrypt)) { // 将明文写入CryptoStream它会自动被加密并流向底层的MemoryStream swEncrypt.Write(plainText); } // StreamWriter关闭时会自动Flush确保所有数据写入CryptoStream } // CryptoStream关闭时会执行最终的加密块和填充 // 此时msEncrypt中包含了IV(16字节) 密文数据 encryptedBytes msEncrypt.ToArray(); } } // Aes对象被释放清除内存中的密钥等敏感信息 // 将包含IV和密文的字节数组转换为Base64字符串便于传输和存储 return Convert.ToBase64String(encryptedBytes); } /// summary /// 使用AES-CBC模式解密密文字符串 /// /summary /// param namecipherTextBase64编码的密文字符串其前16字节必须为IV/param /// param namekey密钥字节数组必须与加密时使用的密钥相同/param /// returns解密后的原始明文字符串/returns /// exception crefArgumentNullException密文或密钥为空/exception /// exception crefFormatException密文不是有效的Base64字符串/exception /// exception crefCryptographicException密钥错误、IV缺失、密文被篡改或填充错误/exception public static string DecryptString(string cipherText, byte[] key) { if (string.IsNullOrEmpty(cipherText)) throw new ArgumentNullException(nameof(cipherText)); if (key null || key.Length 0) throw new ArgumentNullException(nameof(key)); // 将Base64密文转换回字节数组 byte[] fullCipherBytes; try { fullCipherBytes Convert.FromBase64String(cipherText); } catch (FormatException ex) { throw new FormatException(提供的密文不是有效的Base64格式。, ex); } // 检查密文长度是否至少包含一个IV16字节 if (fullCipherBytes.Length BLOCK_SIZE_BYTES) throw new CryptographicException(密文长度过短无法提取IV。密文可能已损坏。); string plaintext null; using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.CBC; aesAlg.Padding PaddingMode.PKCS7; // 从密文字节数组的前16字节提取IV byte[] iv new byte[BLOCK_SIZE_BYTES]; Array.Copy(fullCipherBytes, 0, iv, 0, BLOCK_SIZE_BYTES); aesAlg.IV iv; // 创建解密器 ICryptoTransform decryptor aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); // 准备内存流其中包含IV之后的实际密文数据 using (MemoryStream msDecrypt new MemoryStream(fullCipherBytes, BLOCK_SIZE_BYTES, fullCipherBytes.Length - BLOCK_SIZE_BYTES)) { using (CryptoStream csDecrypt new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (StreamReader srDecrypt new StreamReader(csDecrypt)) { // 从CryptoStream读取数据它会自动解密 plaintext srDecrypt.ReadToEnd(); } } } } return plaintext; } /// summary /// 生成指定长度的随机密钥密码学安全 /// /summary /// param namekeySizeBits密钥长度单位比特。必须是128, 192, 256之一。/param /// returns随机生成的密钥字节数组/returns public static byte[] GenerateRandomKey(int keySizeBits 256) { if (!IsValidKeySize(keySizeBits)) throw new ArgumentException(无效的密钥长度。支持的密钥长度为128, 192, 256比特。, nameof(keySizeBits)); byte[] key new byte[keySizeBits / 8]; // 比特转字节 using (RandomNumberGenerator rng RandomNumberGenerator.Create()) { rng.GetBytes(key); // 用密码学安全的随机数生成器填充数组 } return key; } /// summary /// 使用PBKDF2Password-Based Key Derivation Function 2从密码和盐派生密钥。 /// 这比直接使用密码的哈希值作为密钥更安全能有效抵御彩虹表攻击。 /// /summary /// param namepassword用户输入的密码/param /// param namesalt盐值字节数组应随机生成并保存/param /// param namekeySizeBits期望的密钥长度比特/param /// param nameiterations哈希迭代次数增加计算成本以抵御暴力破解。建议至少10000次。/param /// returns派生出的密钥字节数组/returns public static byte[] DeriveKeyFromPassword(string password, byte[] salt, int keySizeBits 256, int iterations 100000) { if (string.IsNullOrEmpty(password)) throw new ArgumentNullException(nameof(password)); if (salt null || salt.Length 8) throw new ArgumentException(盐值至少需要8字节长度以提高安全性。, nameof(salt)); using (var deriveBytes new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256)) { return deriveBytes.GetBytes(keySizeBits / 8); } } /// summary /// 验证密钥长度是否有效 /// /summary private static bool IsValidKeySize(int keySizeBits) { return keySizeBits 128 || keySizeBits 192 || keySizeBits 256; } } }关键代码解析与设计考量using语句的至关重要Aes、CryptoStream、RandomNumberGenerator等类型都实现了IDisposable接口因为它们内部可能持有非托管资源如加密上下文、随机数生成器句柄。使用using语句确保即使在加密解密过程中发生异常这些资源也能被及时、正确地释放避免内存泄漏和潜在的安全风险例如密钥残留在内存中。IV的处理策略在EncryptString中我们没有手动设置aesAlg.IV而是让Aes.Create()自动生成一个密码学安全的随机IV。这个IV通过msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length)被写入密文流的最前端。解密时我们再从密文的前16字节把它读出来。这是一种非常通用且可靠的做法。CryptoStream的流向这是最容易混淆的点。在加密时我们创建CryptoStream的模式是CryptoStreamMode.Write并将MemoryStream作为目标流。这意味着当我们向CryptoStream写入明文时它会被加密并“写入”底层的MemoryStream。在解密时模式是CryptoStreamMode.Read将包含密文的MemoryStream作为源。当我们从CryptoStream读取时它会自动解密数据并返回给我们。异常处理工具类对输入参数进行了严格的校验并抛出了含义明确的异常类型ArgumentNullException,ArgumentException,FormatException,CryptographicException。这有助于调用者快速定位问题。例如CryptographicException通常意味着密钥错误、密文被篡改或填充不正确。密钥派生DeriveKeyFromPassword这是一个非常重要的安全增强功能。用户通常喜欢用容易记忆的密码但密码的熵随机性很低直接作为AES密钥强度不够。PBKDF2算法通过加入随机盐Salt和多次哈希迭代能将一个弱密码扩展成一个强密钥。盐值必须是随机且唯一的并且需要和密文一起保存以便后续用同样的密码和盐能派生出相同的密钥。4. 实战使用教程与场景示例有了工具类我们来看看如何在各种实际场景中使用它。我会提供从简单到复杂的几个例子。4.1 基础用法加密解密字符串这是最常见的场景比如加密一个API令牌。using System; using System.Text; using YourProject.Security; // 引入我们的工具类命名空间 class Program { static void Main() { // 场景加密一个敏感配置值例如数据库连接字符串的一部分 string originalConnectionString ServermyServer;DatabasemyDb;User IdmyUser;PasswordSuperSecret123;; // **关键步骤1准备密钥** // 警告切勿在源码中硬编码密钥这里仅为演示。 // 正确做法是从环境变量、Azure Key Vault、经过加密的配置文件等安全位置读取。 byte[] key Encoding.UTF8.GetBytes(ThisIsA32ByteLongKey-256Bit!!); // 32字节 256位 Console.WriteLine($原始字符串: {originalConnectionString}); Console.WriteLine($密钥 (Base64): {Convert.ToBase64String(key)}); try { // **关键步骤2执行加密** string encryptedText AesCbcHelper.EncryptString(originalConnectionString, key); Console.WriteLine($加密后 (Base64): {encryptedText}); Console.WriteLine($密文长度: {encryptedText.Length} 字符); // **关键步骤3执行解密** string decryptedText AesCbcHelper.DecryptString(encryptedText, key); Console.WriteLine($解密后: {decryptedText}); // 验证 Console.WriteLine($解密是否成功: {originalConnectionString decryptedText}); } catch (Exception ex) { Console.WriteLine($加解密过程中出现错误: {ex.Message}); Console.WriteLine($异常类型: {ex.GetType().Name}); if (ex.InnerException ! null) { Console.WriteLine($内部异常: {ex.InnerException.Message}); } } } }运行结果与说明原始字符串: ServermyServer;DatabasemyDb;User IdmyUser;PasswordSuperSecret123; 密钥 (Base64): VGhpc0lzQTMyQnl0ZUxvbmdLZXktMjU2Qml0ISE 加密后 (Base64): tzvJg1Lk...很长一串Base64字符每次运行都不同因为IV随机 解密后: ServermyServer;DatabasemyDb;User IdmyUser;PasswordSuperSecret123; 解密是否成功: True你会注意到每次运行加密得到的Base64密文都不同这正是因为IV是随机生成的。但使用相同的密钥我们总能正确解密出原始内容。4.2 进阶用法使用密码派生密钥与文件加密更安全的做法是使用一个用户密码然后派生密钥。同时我们扩展工具类使其能处理文件流。首先为工具类添加文件加密解密方法可选但很实用// 在 AesCbcHelper 类中添加以下方法 /// summary /// 加密文件 /// /summary public static void EncryptFile(string inputFilePath, string outputFilePath, byte[] key) { using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.CBC; // IV自动生成 using (FileStream fsInput new FileStream(inputFilePath, FileMode.Open, FileAccess.Read)) using (FileStream fsOutput new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) { // 将IV写入输出文件开头 fsOutput.Write(aesAlg.IV, 0, aesAlg.IV.Length); using (CryptoStream cs new CryptoStream(fsOutput, aesAlg.CreateEncryptor(), CryptoStreamMode.Write)) { fsInput.CopyTo(cs); // 将输入文件流加密后写入输出流 } } } } /// summary /// 解密文件 /// /summary public static void DecryptFile(string inputFilePath, string outputFilePath, byte[] key) { using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.CBC; using (FileStream fsInput new FileStream(inputFilePath, FileMode.Open, FileAccess.Read)) { // 从加密文件开头读取IV byte[] iv new byte[BLOCK_SIZE_BYTES]; fsInput.Read(iv, 0, iv.Length); aesAlg.IV iv; using (CryptoStream cs new CryptoStream(fsInput, aesAlg.CreateDecryptor(), CryptoStreamMode.Read)) using (FileStream fsOutput new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) { cs.CopyTo(fsOutput); // 将解密后的流写入输出文件 } } } }然后演示如何使用密码派生密钥来加密一个配置文件using System; using System.IO; using System.Text; class AdvancedDemo { static void Main() { // 场景用户用密码“MyMasterPassword”来保护一个本地配置文件。 string password MyMasterPassword; string configContent {ApiEndpoint: https://api.example.com, ApiKey: xyz789abc}; string configFilePath appsettings.encrypted.json; string saltFilePath salt.bin; byte[] salt; byte[] derivedKey; // **情况A首次加密需要生成并保存盐值** if (!File.Exists(saltFilePath)) { Console.WriteLine(首次运行生成新的盐值...); // 生成一个随机盐至少8字节这里用16字节 salt AesCbcHelper.GenerateRandomKey(128); // 128位 16字节用作盐 File.WriteAllBytes(saltFilePath, salt); Console.WriteLine($盐值已生成并保存至: {saltFilePath} (Base64: {Convert.ToBase64String(salt)})); // 使用PBKDF2从密码和盐派生密钥 derivedKey AesCbcHelper.DeriveKeyFromPassword(password, salt, 256, 100000); Console.WriteLine($密钥已从密码派生。); // 加密配置文件内容并保存 string encryptedConfig AesCbcHelper.EncryptString(configContent, derivedKey); File.WriteAllText(configFilePath, encryptedConfig); Console.WriteLine($配置文件已加密保存至: {configFilePath}); } else { // **情况B后续运行读取已保存的盐值** Console.WriteLine(读取已保存的盐值...); salt File.ReadAllBytes(saltFilePath); Console.WriteLine($读取盐值: {Convert.ToBase64String(salt)}); // 使用相同的密码和盐派生相同的密钥 derivedKey AesCbcHelper.DeriveKeyFromPassword(password, salt, 256, 100000); // 读取并解密密文 string encryptedConfig File.ReadAllText(configFilePath); try { string decryptedConfig AesCbcHelper.DecryptString(encryptedConfig, derivedKey); Console.WriteLine($配置文件解密成功内容: {decryptedConfig}); // 这里可以将decryptedConfig反序列化为配置对象使用 } catch (CryptographicException) { Console.WriteLine(解密失败密码错误或文件已损坏。); } } Console.WriteLine(\n--- 演示文件加密 ---); // 加密一个文本文件 string originalFile original.txt; string encryptedFile original.encrypted; string decryptedFile decrypted.txt; File.WriteAllText(originalFile, 这是一个需要加密的敏感文件内容。\n第二行。); Console.WriteLine($原始文件 {originalFile} 已创建。); // 使用一个固定的密钥仅演示实际应用应安全管理密钥 byte[] fileKey Encoding.UTF8.GetBytes(16ByteKeyForFile!!); // 16字节 128位 AesCbcHelper.EncryptFile(originalFile, encryptedFile, fileKey); Console.WriteLine($文件已加密为 {encryptedFile}。); AesCbcHelper.DecryptFile(encryptedFile, decryptedFile, fileKey); string decryptedContent File.ReadAllText(decryptedFile); Console.WriteLine($文件已解密为 {decryptedFile}内容: {decryptedContent}); } }这个示例的精髓在于盐值Salt的持久化盐值不需要保密但必须唯一且与派生出的密钥绑定。我们将其保存到文件salt.bin。即使攻击者拿到了这个盐没有密码也无法推出密钥。高迭代次数DeriveKeyFromPassword中的iterations参数设置为100000这大大增加了从密码暴力推导密钥的计算成本有效抵御攻击。文件流处理EncryptFile和DecryptFile方法直接操作文件流避免了将整个大文件加载到内存中适合处理大型文件。5. 常见问题、排查技巧与性能优化在实际集成和使用过程中你肯定会遇到各种问题。下面是我踩过坑后总结出来的经验。5.1 典型异常与解决方案速查表异常信息可能原因排查步骤与解决方案System.ArgumentNullException: Value cannot be null.调用方法时传入了null的明文、密文或密钥参数。1. 检查传入的字符串或字节数组是否为null或空。2. 在调用加密/解密前添加必要的空值检查。System.ArgumentException: 无效的密钥长度提供的密钥字节数组长度不是16、24或32字节。1. 打印或记录密钥的长度key.Length。2. 确保密钥源如配置文件、环境变量提供的是正确长度的原始字节或能正确解码的Base64字符串。3. 使用GenerateRandomKey方法生成标准长度的密钥。System.FormatException: 提供的密文不是有效的Base64格式尝试解密的字符串不是合法的Base64编码。1. 密文可能在传输或存储过程中被修改如空格、换行符。2. 确认你解密的对象确实是加密方法输出的Base64字符串而不是其他编码或原始字节。3. 尝试在解密前对密文字符串进行Trim()操作。System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.这是最常见的解密错误1.密钥错误99%的情况是解密用的密钥与加密时不同。仔细核对密钥来源和值。2.IV不匹配密文的前16字节不是加密时使用的IV或者你在解密时手动设置了错误的IV。我们的工具类自动处理IV通常不是这里的问题除非你修改了代码。3.密文被篡改密文在传输或存储后发生了改变哪怕一个字符都不行。4.编码问题加密和解密过程中使用的字符编码不一致。确保EncryptString和DecryptString都使用默认的UTF-8通过StreamReader/StreamWriter。System.Security.Cryptography.CryptographicException: Length of the data to decrypt is invalid.密文长度不符合AES块大小的倍数或者提取IV后剩余的数据长度异常。1. 密文可能不完整被截断或损坏。2. 确认Base64解码后的字节数组长度至少为16IV 16至少一个加密块 32字节。解密后得到乱码或部分正确文本通常是由于编码问题或数据截断。1. 如果你加密的是二进制数据如图片请使用EncryptBytes和DecryptBytes变体工具类可扩展而不是字符串方法。字符串方法适用于文本。2. 检查网络传输或文件读写时是否以二进制模式进行避免字符编码转换。5.2 性能考量与最佳实践Aes实例的生命周期对于单次加密/解密操作使用using语句创建和释放Aes对象是没问题的。但是如果你需要在短时间内例如一个Web请求中反复加密大量小数据块反复创建Aes对象和CreateEncryptor/Decryptor会有开销。在这种情况下可以考虑将ICryptoTransform加密器/解密器对象缓存起来复用。但要注意CBC模式下IV每次必须不同所以即使复用ICryptoTransform每次加密也需要指定新的IV。// 性能优化示例缓存Aes对象和加密器仅当IV由外部提供且每次不同时可考虑 private static Aes _cachedAes; private static ICryptoTransform _cachedEncryptor; private static object _lock new object(); public static byte[] EncryptFast(byte[] plainData, byte[] key, byte[] iv) { lock (_lock) { if (_cachedAes null || !_cachedAes.Key.SequenceEqual(key)) { _cachedAes?.Dispose(); _cachedEncryptor?.Dispose(); _cachedAes Aes.Create(); _cachedAes.Key key; _cachedAes.Mode CipherMode.CBC; _cachedAes.Padding PaddingMode.PKCS7; // 注意这里不设置IV因为IV每次由参数传入 _cachedEncryptor _cachedAes.CreateEncryptor(); } // 使用传入的IV和缓存的加密器 using (MemoryStream ms new MemoryStream()) using (CryptoStream cs new CryptoStream(ms, _cachedEncryptor, CryptoStreamMode.Write)) { ms.Write(iv, 0, iv.Length); // 写入IV cs.Write(plainData, 0, plainData.Length); cs.FlushFinalBlock(); return ms.ToArray(); } } } // 注意此示例较复杂需谨慎处理线程安全和对象生命周期。对于大多数应用每次创建新对象更简单安全。密钥存储再次强调安全存储密钥是系统安全的基石。可以考虑的方案开发/测试环境使用用户机密如.NET的Secret Manager或环境变量。生产环境使用专业的密钥管理服务如Azure Key Vault、AWS KMS、HashiCorp Vault等。这些服务提供密钥的生成、轮换、访问审计等功能。备份将密钥的备份保存在极度安全的离线位置。算法与模式选择AES-CBC能提供机密性但不提供完整性校验和认证。这意味着攻击者虽然不能读懂密文但有可能篡改密文例如调换两个密文块导致解密出的明文是混乱的但解密过程可能不会报错除非填充错误。如果需要对密文的完整性和来源进行认证应考虑使用AES-GCMGalois/Counter Mode模式它同时提供加密和认证。.NET Core 3.0 和 .NET 5 原生支持AesGcm类。如果你的场景对安全性要求极高如金融交易GCM是更推荐的选择。5.3 扩展工具类处理字节数组与非文本数据我们的基础工具类只处理字符串。但很多时候我们需要加密二进制数据如序列化的对象、图片缩略图等。添加对应的字节数组方法非常简单且必要。// 在 AesCbcHelper 类中添加 /// summary /// 加密字节数组 /// /summary public static byte[] EncryptBytes(byte[] plainData, byte[] key) { // 参数校验... using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.CBC; using (MemoryStream ms new MemoryStream()) using (ICryptoTransform encryptor aesAlg.CreateEncryptor()) { ms.Write(aesAlg.IV, 0, aesAlg.IV.Length); using (CryptoStream cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(plainData, 0, plainData.Length); cs.FlushFinalBlock(); } return ms.ToArray(); } } } /// summary /// 解密密文字节数组前16字节为IV /// /summary public static byte[] DecryptBytes(byte[] cipherDataWithIv, byte[] key) { // 参数校验检查长度... using (Aes aesAlg Aes.Create()) { aesAlg.Key key; aesAlg.Mode CipherMode.CBC; byte[] iv new byte[BLOCK_SIZE_BYTES]; Array.Copy(cipherDataWithIv, 0, iv, 0, iv.Length); aesAlg.IV iv; using (MemoryStream ms new MemoryStream()) using (ICryptoTransform decryptor aesAlg.CreateDecryptor()) using (MemoryStream cipherStream new MemoryStream(cipherDataWithIv, BLOCK_SIZE_BYTES, cipherDataWithIv.Length - BLOCK_SIZE_BYTES)) using (CryptoStream cs new CryptoStream(cipherStream, decryptor, CryptoStreamMode.Read)) { cs.CopyTo(ms); return ms.ToArray(); } } }有了这两个方法你就可以轻松加密任何可序列化的对象// 加密一个自定义对象 MyConfig config new MyConfig { ApiKey secret123, Expiry DateTime.UtcNow.AddDays(1) }; byte[] serializedData; using (MemoryStream ms new MemoryStream()) { // 使用你喜欢的序列化库如 System.Text.Json, Newtonsoft.Json, 或 BinaryFormatter var json System.Text.Json.JsonSerializer.Serialize(config); serializedData Encoding.UTF8.GetBytes(json); } byte[] key AesCbcHelper.GenerateRandomKey(); byte[] encryptedData AesCbcHelper.EncryptBytes(serializedData, key); // 存储或传输 encryptedData // 解密时 byte[] decryptedData AesCbcHelper.DecryptBytes(encryptedData, key); string jsonText Encoding.UTF8.GetString(decryptedData); MyConfig decryptedConfig System.Text.Json.JsonSerializer.DeserializeMyConfig(jsonText);封装这个AesCbcHelper工具类的过程让我对C#的加密体系和安全编程的细节有了更深刻的理解。最大的体会是加密本身并不复杂System.Security.Cryptography已经封装得很好真正的挑战在于如何正确地、安全地管理密钥和集成到应用流程中。永远不要低估“安全”二字的分量一个看似微小的疏忽比如硬编码密钥、使用固定IV就可能导致整个安全机制形同虚设。希望这个工具类和详细的教程能帮你扫清障碍更自信地在你的C#项目中实现数据安全保护。如果在使用中遇到其他问题不妨从密钥和IV的一致性这个源头开始排查十有八九能找到答案。