C# AES加密解密实战:从原理到跨平台应用与性能优化

C# AES加密解密实战:从原理到跨平台应用与性能优化
1. 项目概述为什么AES是C#开发者的必备技能在C#开发中数据安全从来都不是一个可选项而是底线。无论是处理用户密码、保护配置文件中的数据库连接字符串还是实现客户端与服务器之间的安全通信加密解密都是绕不开的核心环节。在众多加密算法中AES高级加密标准因其安全性高、效率出色且被广泛支持成为了事实上的行业标准。你可能在无数个项目中见过它但真正理解其内部机制并能根据实际场景灵活、正确地运用它却是区分普通开发者和资深工程师的一道分水岭。这个项目标题“C#实现AES加密解密技术详解”其核心价值远不止于贴出几行调用System.Security.Cryptography命名空间下类的代码。它要求我们深入理解AES在C#生态中的完整实现路径从基础的ECB模式到更安全的CBC模式从密钥和IV初始化向量的生成与管理到处理填充Padding和编码Encoding这些看似琐碎却极易引发安全漏洞或兼容性问题的细节。网络上充斥着大量“复制即用”但存在隐患的代码片段比如使用固定密钥、忽略IV、或错误处理密文编码导致跨平台解密失败。本文将从一个有十年经验的C#开发者视角不仅带你手把手实现健壮的AES加密解密工具类更会深入每个参数背后的设计意图分享我在金融、物联网数据传输等严苛场景下积累的实战经验和避坑指南。2. AES核心原理与C#实现框架解析2.1 AES算法简析不只是“黑盒”调用在动手写代码前花几分钟理解AES的基本原理至关重要这能帮助你在出现问题时快速定位而不是盲目试错。AES是一种对称分组加密算法意味着加密和解密使用同一把密钥。它处理数据时会将明文分割成固定128位16字节的块然后经过多轮10, 12或14轮取决于密钥长度是128, 192或256位的替换、移位、列混合和轮密钥加等操作最终输出密文块。对于C#开发者而言最需要关注的是其工作模式和填充模式。.NET 主要支持以下几种ECB (Electronic Codebook)最简单的模式每个数据块独立加密。致命缺点相同的明文块会产生相同的密文块无法隐藏数据模式安全性很低绝不应用于需要保密性的场景通常仅用于测试或某些特定格式要求如某些硬件加密芯片。CBC (Cipher Block Chaining)推荐使用的默认模式。每个明文块在加密前会先与前一个密文块进行异或操作。第一个块则使用一个随机生成的初始化向量IV。这确保了即使明文相同产生的密文也完全不同且模式被隐藏。IV无需保密但必须随机且唯一通常每次加密都随机生成并随密文一起传输。其他模式如CFB、OFB等在.NET中也有支持但CBC因其安全性和广泛兼容性是通用场景下的首选。填充Padding是为了解决最后一个明文块不足128位16字节的问题。.NET中常用的有PKCS7默认和None。PKCS7会填充缺少的字节数例如缺5个字节就填充5个值为5的字节。选择None则要求你的明文长度必须是块大小的整数倍否则会抛出异常。2.2 C#中的AES类AesvsAesManagedvsAesCryptoServiceProvider在System.Security.Cryptography命名空间下你会遇到几个类似的类它们的选择体现了.NET框架的演进Aes这是一个抽象基类也是目前.NET Core/.NET 5 以及 .NET Framework 4.6推荐使用的类型。它提供了创建平台最优实现工厂方法Aes.Create()。使用它能获得最好的性能和跨平台兼容性。AesManaged如其名这是一个完全在托管代码C#中实现的AES类。在早期.NET中当某些环境没有本地加密支持时会用到它。现在它的性能通常不如基于本地API的实现如AesCryptoServiceProvider除非你有明确的理由如避免调用本地代码否则不推荐作为首选。AesCryptoServiceProvider基于Windows CryptoAPI的实现在Windows平台上通常有较好的性能。但在跨平台如Linux场景下Aes.Create()会自动选择更适合当前系统的实现。实操心得在新项目中无脑使用Aes.Create()来获取实例。这保证了代码能自动适配运行环境获得最佳实现。这是编写可移植、高性能加密代码的第一步好习惯。// 正确做法使用工厂方法创建AES实例 using System.Security.Cryptography; public static Aes CreateAesInstance() { // 这将返回当前平台下最优的AES实现 Aes aes Aes.Create(); // 后续配置密钥、模式等 aes.KeySize 256; // 设置密钥长度 aes.Mode CipherMode.CBC; // 设置工作模式 aes.Padding PaddingMode.PKCS7; // 设置填充模式 return aes; }3. 构建健壮的AES加密解密工具类3.1 密钥与IV的生成与管理安全性的基石密钥是加密的灵魂其管理和生成方式直接决定了整个体系的安全性。1. 生成强随机密钥和IV永远不要自己用字符串如“mySecretKey123”拼接或使用弱随机数生成器来创建密钥。应使用密码学安全的随机数生成器CSPRNG。在C#中RNGCryptoServiceProvider或其更现代的替代品RandomNumberGenerator类就是为此而生。using System.Security.Cryptography; public static (byte[] key, byte[] iv) GenerateKeyAndIV(int keySize 256) { // keySize: 128, 192, 256 using Aes aes Aes.Create(); aes.KeySize keySize; aes.GenerateKey(); // 自动生成随机密钥 aes.GenerateIV(); // 自动生成随机IV return (aes.Key, aes.IV); }2. 从密码派生密钥Password-Based Key Derivation Function, PBKDF2很多时候密钥来源于用户输入的密码。直接对密码进行哈希如SHA256作为密钥是不安全的因为哈希输出是确定的且密码熵值可能不足。正确的方法是使用像PBKDF2、bcrypt或Argon2这样的密钥派生函数它们会引入盐值Salt并进行多次迭代极大增加暴力破解的难度。using System.Security.Cryptography; using System.Text; public static (byte[] key, byte[] iv, byte[] salt) DeriveKeyFromPassword(string password, int keySize 256) { // 生成一个随机的盐值Salt byte[] salt new byte[16]; // 通常16字节足够 using RandomNumberGenerator rng RandomNumberGenerator.Create(); rng.GetBytes(salt); // 使用Rfc2898DeriveBytes实现了PBKDF2派生密钥 // 参数密码盐值迭代次数建议至少10万次哈希算法 using var deriveBytes new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256); // AES-256需要32字节密钥CBC模式需要16字节IV byte[] key deriveBytes.GetBytes(keySize / 8); // 256位 - 32字节 byte[] iv deriveBytes.GetBytes(16); // 固定16字节 return (key, iv, salt); // 盐值必须和密文一起保存用于后续解密 }注意盐值Salt必须是随机且唯一的它的目的不是保密而是确保即使用户密码相同派生出的密钥也不同防止彩虹表攻击。盐值需要和密文一起存储或传输。3. 密钥的存储绝对不要将密钥硬编码在源代码中或存储在配置文件明文里。对于服务器应用应使用安全的密钥管理系统如Azure Key Vault, AWS KMS或受保护的存储如Windows DPAPI。对于客户端应用需权衡安全与便利可以考虑使用操作系统提供的凭据存储或让用户每次输入。3.2 核心加密与解密方法实现掌握了密钥管理我们就可以实现核心的加密解密方法了。我们将实现两个最通用的方法一个用于加密字节数组一个用于加密字符串因为文本处理更常见。同时我们会处理好IV的携带问题。方法一加密/解密字节数组最底层using System; using System.IO; using System.Security.Cryptography; public static class AesHelper { /// summary /// 使用AES-CBC模式加密字节数组 /// /summary /// param nameplainBytes明文字节数组/param /// param namekey密钥必须为128, 192或256位/param /// param nameiv初始化向量必须为16字节如果为null则随机生成/param /// returns包含IV前16字节和密文的字节数组/returns public static byte[] EncryptBytes(byte[] plainBytes, byte[] key, byte[] iv null) { if (plainBytes null || plainBytes.Length 0) throw new ArgumentNullException(nameof(plainBytes)); if (key null || (key.Length ! 16 key.Length ! 24 key.Length ! 32)) throw new ArgumentException(Key must be 16, 24, or 32 bytes (128, 192, 256 bits)., nameof(key)); using Aes aes Aes.Create(); aes.Key key; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; // 如果未提供IV则生成一个随机的 if (iv null) { aes.GenerateIV(); iv aes.IV; } else { if (iv.Length ! 16) // AES块大小是16字节 throw new ArgumentException(IV must be 16 bytes., nameof(iv)); aes.IV iv; } using ICryptoTransform encryptor aes.CreateEncryptor(); using MemoryStream ms new MemoryStream(); // 重要先将IV写入流的前端 ms.Write(iv, 0, iv.Length); using (CryptoStream cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(plainBytes, 0, plainBytes.Length); // CryptoStream在Dispose时会自动FlushFinalBlock处理最后的填充 } // 此时ms包含了 IV 密文 return ms.ToArray(); } /// summary /// 解密由EncryptBytes方法加密的字节数组 /// /summary /// param namecipherBytesWithIv包含IV前16字节和密文的字节数组/param /// param namekey密钥/param /// returns解密后的明文字节数组/returns public static byte[] DecryptBytes(byte[] cipherBytesWithIv, byte[] key) { // 参数检查... if (cipherBytesWithIv null || cipherBytesWithIv.Length 17) // 至少IV(16)1字节密文 throw new ArgumentException(Invalid cipher data., nameof(cipherBytesWithIv)); using Aes aes Aes.Create(); aes.Key key; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; // 从数据流前16字节提取IV byte[] iv new byte[16]; Buffer.BlockCopy(cipherBytesWithIv, 0, iv, 0, 16); aes.IV iv; // 剩余部分是真正的密文 int cipherLength cipherBytesWithIv.Length - 16; byte[] cipherText new byte[cipherLength]; Buffer.BlockCopy(cipherBytesWithIv, 16, cipherText, 0, cipherLength); using ICryptoTransform decryptor aes.CreateDecryptor(); using MemoryStream ms new MemoryStream(cipherText); using CryptoStream cs new CryptoStream(ms, decryptor, CryptoStreamMode.Read); using MemoryStream outputMs new MemoryStream(); cs.CopyTo(outputMs); return outputMs.ToArray(); } }关键点解析IV的处理加密时我们将随机生成的IV预先拼接到密文前面。这是一种常见且简单的做法确保解密方能拿到同样的IV。你也可以通过其他安全信道传输IV但和密文一起存储最为方便。CryptoStream的用法注意CryptoStream的构造和using语句的嵌套。在写入完成后必须确保CryptoStream被正确关闭Dispose这会触发它执行最后的填充操作FlushFinalBlock。如果忘记关闭解密时可能会遇到“填充无效无法被移除”的错误。字节操作使用Buffer.BlockCopy进行字节数组切片效率高于LINQ。方法二加密/解密字符串更常用字符串需要编码为字节数组才能加密密文字节数组也通常需要转换为可打印或可传输的格式如Base64或Hex。public static class AesHelper { // ... 上面的字节数组方法 ... /// summary /// 加密字符串返回Base64格式的字符串包含IV /// /summary public static string EncryptString(string plainText, byte[] key, Encoding encoding null) { if (string.IsNullOrEmpty(plainText)) return string.Empty; encoding ?? Encoding.UTF8; // 默认使用UTF-8编码 byte[] plainBytes encoding.GetBytes(plainText); byte[] encryptedBytesWithIv EncryptBytes(plainBytes, key); // 调用上面的方法 return Convert.ToBase64String(encryptedBytesWithIv); } /// summary /// 解密Base64格式的字符串包含IV /// /summary public static string DecryptString(string base64CipherText, byte[] key, Encoding encoding null) { if (string.IsNullOrEmpty(base64CipherText)) return string.Empty; encoding ?? Encoding.UTF8; try { byte[] cipherBytesWithIv Convert.FromBase64String(base64CipherText); byte[] decryptedBytes DecryptBytes(cipherBytesWithIv, key); return encoding.GetString(decryptedBytes); } catch (FormatException) { throw new ArgumentException(Input string is not a valid Base64 string., nameof(base64CipherText)); } catch (CryptographicException ex) { // 典型的解密失败密钥错误、数据被篡改、IV不匹配等 throw new InvalidOperationException(Decryption failed. Check your key and cipher text., ex); } } /// summary /// 基于密码的便捷加密方法使用PBKDF2派生密钥 /// /summary public static (string cipherText, string saltBase64) EncryptStringWithPassword(string plainText, string password) { var (key, iv, salt) DeriveKeyFromPassword(password); // 调用前面定义的密钥派生方法 byte[] plainBytes Encoding.UTF8.GetBytes(plainText); byte[] encryptedBytes EncryptBytes(plainBytes, key, iv); // 使用派生出的key和iv string cipherText Convert.ToBase64String(encryptedBytes); string saltBase64 Convert.ToBase64String(salt); // 注意实际应用中你需要将 saltBase64 和 cipherText 一起存储或传输 return (cipherText, saltBase64); } /// summary /// 基于密码的便捷解密方法 /// /summary public static string DecryptStringWithPassword(string cipherText, string password, string saltBase64) { byte[] salt Convert.FromBase64String(saltBase64); byte[] cipherBytes Convert.FromBase64String(cipherText); // 使用相同的盐值和密码重新派生密钥和IV var (key, iv, _) DeriveKeyFromPassword(password, salt); // 需要重载一个接受盐值的方法 byte[] decryptedBytes DecryptBytes(cipherBytes, key, iv); return Encoding.UTF8.GetString(decryptedBytes); } }实操心得在将字节数组转换为字符串进行传输或存储时Base64是最佳选择。它只使用64个可打印ASCII字符避免了二进制数据在文本协议如JSON、XML、URL中可能出现的编码问题。Hex编码虽然也可读但体积会比Base64大约33%。永远不要尝试直接将密文字节数组用Encoding.GetString转换成字符串这几乎必然会导致数据损坏。4. 高级话题与实战场景剖析4.1 处理大文件与流式加密上述方法将全部数据读入内存MemoryStream这对于大文件如几百MB的视频是不可行的会导致内存溢出。正确的做法是使用流Stream进行分块处理。public static void EncryptFile(string inputFilePath, string outputFilePath, byte[] key, byte[] iv null) { using Aes aes Aes.Create(); // ... 配置aes (Key, ModeCBC, PaddingPKCS7) ... if (iv null) { aes.GenerateIV(); iv aes.IV; } else { aes.IV iv; } using ICryptoTransform encryptor aes.CreateEncryptor(); using FileStream inputFs new FileStream(inputFilePath, FileMode.Open, FileAccess.Read); using FileStream outputFs new FileStream(outputFilePath, FileMode.Create, FileAccess.Write); // 首先将IV写入输出文件开头 outputFs.Write(iv, 0, iv.Length); // 使用CryptoStream包装输出流 using CryptoStream cryptoStream new CryptoStream(outputFs, encryptor, CryptoStreamMode.Write); inputFs.CopyTo(cryptoStream); // 这里会自动进行分块加密 // cryptoStream在Dispose时会自动处理最后的填充和刷新 } public static void DecryptFile(string inputFilePath, string outputFilePath, byte[] key) { using Aes aes Aes.Create(); aes.Key key; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; using FileStream inputFs new FileStream(inputFilePath, FileMode.Open, FileAccess.Read); // 从文件开头读取IV byte[] iv new byte[16]; if (inputFs.Read(iv, 0, 16) ! 16) throw new InvalidDataException(File is too short to contain an IV.); aes.IV iv; using ICryptoTransform decryptor aes.CreateDecryptor(); // 注意CryptoStream现在包装的是输入流密文源模式是Read using CryptoStream cryptoStream new CryptoStream(inputFs, decryptor, CryptoStreamMode.Read); using FileStream outputFs new FileStream(outputFilePath, FileMode.Create, FileAccess.Write); cryptoStream.CopyTo(outputFs); // 自动分块解密并写入 }关键点注意加密和解密时CryptoStream包装的流对象和模式Write或Read是不同的。加密时明文流是输入CryptoStream包装输出流进行写入解密时密文流是输入CryptoStream包装输入流进行读取。CopyTo方法会高效地处理缓冲区避免一次性加载大文件。4.2 跨平台与跨语言兼容性实战一个常见的需求是用C#加密的数据需要用Java、Python或JavaScript解密反之亦然。这要求双方在以下关键参数上严格一致参数必须一致的项目C# (.NET) 典型配置常见问题算法AESAes.Create()确认是AES不是DES或RSA密钥长度128/192/256位KeySize 256双方密钥字节数组长度必须一致工作模式CBC/ECB等Mode CipherMode.CBC最易出错点必须完全相同填充模式PKCS7/PKCS5/None等Padding PaddingMode.PKCS7关键点在AES中PKCS5和PKCS7填充实质相同填充字节的值等于缺少的字节数。但需确认对方库的命名Java常用PKCS5Padding。初始化向量IV值随机生成并随密文传递必须使用相同的IV解密数据编码字符集/二进制格式UTF-8编码Base64输出明文转字节、密文转字符串的编码方式必须一致与Java互操作示例要点Java中常用Cipher.getInstance(AES/CBC/PKCS5Padding)。注意虽然叫PKCS5Padding但在AES的16字节块大小下它与PKCS7是兼容的。确保双方都使用CBC模式并且IV的处理方式一致通常也是拼在密文前。与JavaScriptWeb Crypto API互操作现代浏览器支持Web Crypto API但其较为底层。一个更实用的方法是使用如crypto-js库。你需要确保在JavaScript端使用相同的模式CBC、填充PKCS7和密钥、IV。// 使用 crypto-js 的示例仅供参考 // const CryptoJS require(crypto-js); // 假设从C#端传来了Base64编码的IV和密文以及密钥可能是密码派生的 // let iv CryptoJS.enc.Base64.parse(base64Iv); // let key CryptoJS.enc.Utf8.parse(password); // 或处理成WordArray // let encrypted CryptoJS.enc.Base64.parse(base64CipherText); // // let decrypted CryptoJS.AES.decrypt( // { ciphertext: encrypted }, // key, // { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } // ); // let plaintext decrypted.toString(CryptoJS.enc.Utf8);避坑指南当跨语言解密失败时按以下顺序排查1) 确认Base64解码是否正确2) 检查密钥字节是否完全一致3) 确认IV是否正确提取并设置4) 核对算法、模式、填充的字符串标识是否完全匹配5) 检查明文编码UTF-8 vs UTF-16等。4.3 性能优化与异常处理性能优化重用Aes对象和ICryptoTransform如果需要在循环中加密大量小数据块创建和销毁Aes对象开销较大。可以考虑在线程安全的情况下重用它们。但注意ICryptoTransform加密器/解密器通常不是线程安全的且与特定的Key/IV绑定。使用ArrayPoolbyte在处理流加密解密时用于缓冲的字节数组可以从ArrayPoolbyte.Shared租用以减少GC压力。byte[] buffer ArrayPoolbyte.Shared.Rent(81920); // 租用80KB缓冲区 try { int bytesRead; while ((bytesRead inputStream.Read(buffer, 0, buffer.Length)) 0) { cryptoStream.Write(buffer, 0, bytesRead); } } finally { ArrayPoolbyte.Shared.Return(buffer); }健壮的异常处理加密解密操作可能抛出多种异常必须妥善处理。ArgumentNullException/ArgumentException输入参数无效。CryptographicException这是加解密操作失败的核心异常。可能的原因包括密钥错误长度不对或内容不对。IV长度不正确。密文数据被篡改导致填充验证失败。不支持的加密模式或填充模式。FormatException在Base64解码时抛出。IOException文件流操作失败。在你的工具类或业务逻辑中应该捕获这些异常并根据上下文转换为更有意义的业务异常或给用户友好的提示但不要泄露密钥等敏感信息。public string SafeDecrypt(string encryptedBase64, byte[] key) { try { return AesHelper.DecryptString(encryptedBase64, key); } catch (FormatException) { // 记录日志 throw new BusinessException(数据格式错误无法解密。); } catch (CryptographicException ex) { // 记录日志注意不要记录ex.Message可能包含敏感信息 _logger.LogWarning(ex, 解密失败密钥或数据可能无效。); throw new BusinessException(解密失败请检查输入数据或联系管理员。); } catch (Exception ex) { _logger.LogError(ex, 解密过程中发生未知错误。); throw; } }5. 常见问题排查与调试技巧实录即使按照最佳实践编写代码在实际集成和部署中仍然会遇到各种问题。下面是我在多年支持中总结的几个最常见问题及其解决方法。5.1 “填充无效无法被移除” (Padding is invalid and cannot be removed)这是最经典、最常遇到的CryptographicException。它几乎总意味着解密端和加密端的某个核心参数不匹配。排查清单密钥不一致这是首要怀疑对象。确认用于解密的密钥字节数组与加密时使用的完全一致一个字节都不能差。检查密钥是否被意外截断、编码或修改。IV不一致确认解密时使用的IV与加密时使用的IV完全相同。如果你采用“IV拼在密文前”的方式检查解密代码是否正确地从密文数据的前16字节提取了IV。密文被损坏在传输或存储过程中密文数据可能被截断、修改或编码错误例如Base64字符串在URL传输中//字符被处理。确保接收到的密文完整且正确。工作模式不匹配加密用CBC解密也必须用CBC。检查CipherMode设置。填充模式不匹配加密用PKCS7解密也必须用PKCS7。这是另一个常见错误来源尤其是在与某些默认配置不同的系统交互时。数据块大小问题如果你使用了PaddingMode.None则必须确保明文长度是16字节的整数倍否则加密时就会出错。调试技巧写一个简单的单元测试用相同的密钥和IV加密一个短字符串如Hello然后立即解密。如果失败问题肯定出在代码逻辑上。如果成功但与外部系统交互失败则用日志或调试工具对比双方在加密前和解密前的所有参数密钥Hex、IV Hex、模式、填充的字符串表示以及密文的Hex或Base64值。5.2 跨语言解密时得到的明文是乱码如果解密没有抛出异常但得到的字符串是乱码问题通常出在编码Encoding上。排查步骤确认编码C#端加密时string是用什么Encoding如UTF8,Unicode,ASCII转换成byte[]的解密后必须用完全相同的编码将byte[]转回string。UTF-8是跨平台的首选。检查BOM某些编码如UTF8带BOM会在字节数组开头添加额外字节。确保加解密双方对BOM的处理一致。通常建议使用new UTF8Encoding(false)来创建不带BOM的UTF-8编码器。验证Base64确保在传输过程中Base64字符串没有被额外转义或修改。可以尝试将双方生成的Base64字符串进行逐字符比较。5.3 性能问题加密解密速度慢对于大量数据或高频调用性能可能成为瓶颈。分析与优化定位热点使用性能剖析工具如Visual Studio Profiler、dotTrace确定是CPU瓶颈还是I/O瓶颈。密钥派生是瓶颈如果使用Rfc2898DeriveBytesPBKDF2且迭代次数很高如10万次密钥派生过程本身就会很慢这是设计使然为了增加暴力破解难度。考虑在内存中缓存派生出的密钥而不是每次加解密都重新派生。但务必保证缓存的安全。使用硬件加速现代CPU如Intel AES-NI对AES有硬件指令级加速。确保你的.NET运行时环境支持并启用了这些优化。通常Aes.Create()返回的实现会自动利用硬件加速。异步与流对于文件或网络流务必使用异步方法ReadAsync,WriteAsync,CopyToAsync和流式处理避免阻塞主线程和一次性加载大文件到内存。5.4 内存泄漏与资源管理Aes、ICryptoTransform、CryptoStream、MemoryStream等都实现了IDisposable接口。黄金法则将它们包裹在using语句中以确保即使发生异常非托管资源如加密句柄也能被正确释放。// 正确做法 using Aes aes Aes.Create(); using ICryptoTransform encryptor aes.CreateEncryptor(); using MemoryStream ms new MemoryStream(); using (CryptoStream cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { // ... 操作cs ... } // using 块结束时自动调用Dispose // 错误做法可能导致资源泄漏 Aes aes Aes.Create(); ICryptoTransform encryptor aes.CreateEncryptor(); // ... 如果后续代码抛出异常encryptor和aes可能不会被释放 ...对于在类中持有加密相关对象作为字段的情况确保该类也实现IDisposable并在Dispose方法中清理这些字段。5.5 安全警示常见错误模式使用固定密钥和IV这是安全灾难。永远不要将密钥硬编码在代码中。对于IV每次加密都必须使用新的随机值。使用ECB模式除非你非常清楚自己在做什么例如加密高度结构化的、非机密的数据否则永远不要使用ECB模式。它的模式泄露问题使其不适合加密任何需要保密的数据。自行实现加密算法绝对不要尝试自己写AES或者任何加密算法的实现。使用经过严格审计和广泛测试的库如.NET Framework自带的System.Security.Cryptography。忽略填充预言攻击在CBC模式下某些填充错误信息可能会被攻击者利用填充预言攻击。在公开网络服务中解密用户提供的密文时无论成功与否都应返回统一的、模糊的错误信息如“处理失败”而不是具体的“密钥错误”或“填充错误”。更好的做法是使用认证加密模式如GCM它同时提供保密性和完整性验证。.NET 也支持AesGcm类主要存在于System.Security.Cryptography命名空间中但注意其API与非认证模式略有不同。我个人在实际项目中的体会是AES加密本身并不复杂但“魔鬼在细节中”。一个健壮的加密模块其代码的70%可能都在处理密钥管理、错误处理、编码转换和跨平台兼容性这些“周边”事务。在项目初期就采用本文所述的健壮工具类模式并建立完善的单元测试覆盖正常流程、错误密钥、错误数据、边界情况等能为后续的集成和运维省去无数排查的夜晚。最后记住加密是安全链条中的一环而非全部。密钥的安全存储、传输通道的安全HTTPS、服务器的安全配置同等重要。