C# RSA非对称加密实战:从原理到跨平台对接与性能优化

发布时间:2026/7/16 5:01:47
C# RSA非对称加密实战:从原理到跨平台对接与性能优化 1. 项目概述为什么C#开发者绕不开RSA如果你用C#做过任何涉及数据安全传输或存储的功能比如用户密码令牌、支付接口签名、配置文件加密那你大概率听说过或者用过RSA。这玩意儿就像程序世界的“数字信封”发信人用一把公开的锁公钥把信锁进信封只有持有唯一钥匙私钥的收信人才能打开。在C#的生态里RSACryptoServiceProvider和它的继任者RSA类是我们实现这套“信封”逻辑的直接工具。但说实话我第一次用RSACryptoServiceProvider的时候被那一串XML格式的密钥给整懵了。别的平台比如Java或者OpenSSL生成的密钥常常是一段PEM格式的Base64字符串干净利落。而C#默认吐出来的是裹着一堆XML标签的“臃肿”字符串。这直接导致了一个非常现实的问题当你需要和第三方系统比如一个用Java写的后端服务进行RSA加密通信时密钥格式不匹配对接起来简直是一场灾难。你可能会遇到“RSA public key not find”或者“密钥格式无效”这类让人头疼的错误。所以今天聊的“C#实现RSA非对称加密与解密”远不止是调用两个API那么简单。它至少包含三个核心层面第一如何在C#中正确地生成、使用密钥对进行加密和解密第二如何理解并处理C#特有的XML密钥格式与其他通用格式如PEM、PKCS#8之间的转换第三在实际项目中如何规避那些教科书上不会写的坑比如密钥长度选择、填充模式、以及如何处理分段加密大数据。这些经验都是我在多次跨平台对接和安全性审计中一点点攒下来的。2. 核心原理与.NET中的RSA实现演进在深入代码之前我们得先统一一下思想。RSA算法的安全性建立在“大数质因数分解极其困难”这个数学难题之上。简单说你公开一个非常大的数n它是两个大质数p和q的乘积别人可以用n和另一个公开数e公钥指数来加密数据。但想解密就必须知道p和q从而计算出私钥指数d。对于足够大的n例如2048位以目前的计算能力分解它在时间上是不现实的。.NET框架为我们封装了所有这些复杂的数学运算。但需要注意的是.NET中的RSA实现有过一次重要的演进。2.1 从 RSACryptoServiceProvider 到 RSA 基类在.NET Framework早期以及.NET Core 3.0之前我们主要使用RSACryptoServiceProvider这个类。它位于System.Security.Cryptography命名空间下功能强大但也有一些历史包袱比如它严重依赖Windows的CryptoAPI在非Windows系统上受限虽然.NET Core做了兼容而且默认的密钥导出格式就是XML。// .NET Framework / 传统用法 using (var rsa new RSACryptoServiceProvider(2048)) { string privateKeyXml rsa.ToXmlString(true); // 导出包含私钥的XML string publicKeyXml rsa.ToXmlString(false); // 导出仅含公钥的XML }随着.NET Core和.NET 5的跨平台战略推进微软引入了更抽象、更灵活的RSA抽象基类以及它的标准实现RSA.Create()。这个工厂方法会根据运行平台返回最合适的实现在Windows上可能是RSACryptoServiceProvider的封装在Linux/macOS上可能是基于OpenSSL的实现。更重要的是它支持更现代的密钥格式如PEM。// .NET 5 推荐用法 using System.Security.Cryptography; using (RSA rsa RSA.Create()) { rsa.KeySize 2048; // 设置密钥长度 // 导出为PEM格式.NET 5 开始内置支持 string privateKeyPem rsa.ExportRSAPrivateKeyPem(); string publicKeyPem rsa.ExportRSAPublicKeyPem(); // 或者导出为XML兼容旧代码 string privateKeyXml rsa.ToXmlString(true); }2.2 密钥长度与安全性的权衡在生成密钥时第一个要做的选择就是密钥长度。长度直接关系到安全性。RSACryptoServiceProvider的构造函数接受一个密钥长度参数单位是比特bit。1024位目前已被认为是不安全的许多新的安全标准如PCI DSS已明确禁止使用。仅在遗留内部系统中可能见到。2048位当前绝对的主流和最低安全要求。广泛应用于HTTPSTLS、代码签名、电子邮件加密等场景。在可预见的未来十几年内是安全的。3072位更高安全级别的要求用于保护有效期非常长的数据或应对未来算力提升。4096位及以上用于最高安全级别的场景但加解密性能会显著下降。实操心得对于绝大多数应用无脑选择2048位即可。除非你有明确的合规性要求比如金融行业某些规范要求3072位否则不要盲目使用更长的密钥因为加解密速度会变慢而且有些老旧系统或库可能不支持超长密钥。生成密钥是一个比较耗CPU的操作尽量避免在每次请求时动态生成。2.3 填充模式PKCS#1 v1.5 与 OAEP当你调用Encrypt或Decrypt方法时通常会看到一个名为fOAEP在旧API中或padding的参数。这指定了RSA加密的填充模式。填充是必须的因为RSA算法本身是确定性的对同样的明文和密钥总会产生同样的密文。不加填充容易受到多种攻击。PKCS#1 v1.5 Padding (fOAEP: false)一种较老的填充方案。它在历史上被广泛使用但已知存在一些潜在的理论漏洞如Bleichenbacher攻击。在现代应用中不推荐使用。OAEP Padding (fOAEP: true / RSAEncryptionPadding.OaepSHA1)最优非对称加密填充。它提供了更强的安全性是当前推荐的标准。在.NET中通常与SHA-1或SHA-256等哈希算法结合使用如RSAEncryptionPadding.OaepSHA256。// 现代、安全的加密方式.NET Core / .NET 5 byte[] dataToEncrypt Encoding.UTF8.GetBytes(Hello, RSA!); byte[] encryptedData rsa.Encrypt(dataToEncrypt, RSAEncryptionPadding.OaepSHA256);注意事项填充模式在加密和解密时必须严格匹配。如果你用OaepSHA256加密就必须用OaepSHA256解密。用错填充模式是导致解密失败最常见的原因之一错误信息通常是“无效的数据”或“填充无效无法被移除”。在跨平台交互时双方必须明确约定并统一使用相同的填充模式通常OAEP是首选。3. 从生成到使用一套完整的C# RSA加解密流程理论说再多不如一行代码。我们从一个完整的、可运行的控制台示例开始看看在.NET 6的环境下如何完成密钥生成、加密、解密的闭环。3.1 密钥生成与存储首先我们生成一对2048位的RSA密钥并分别以XML和PEM两种格式导出保存。这能让你直观地看到两种格式的区别。using System.Security.Cryptography; using System.Text; namespace RsaDemo { class Program { static void Main(string[] args) { // 1. 创建RSA实例并生成密钥 using (RSA rsa RSA.Create(2048)) // 明确指定2048位长度 { Console.WriteLine($生成的RSA密钥长度: {rsa.KeySize} 位); // 2. 导出各种格式的密钥 // XML格式C#传统格式包含所有参数 string privateKeyXml rsa.ToXmlString(true); string publicKeyXml rsa.ToXmlString(false); // PEM格式.NET 5 原生支持跨平台通用 string privateKeyPem rsa.ExportRSAPrivateKeyPem(); // PKCS#1 格式 string publicKeyPem rsa.ExportRSAPublicKeyPem(); // 也可以导出为PKCS#8格式的PEM更通用 string privateKeyPkcs8Pem rsa.ExportPkcs8PrivateKeyPem(); Console.WriteLine( 私钥 (XML) ); Console.WriteLine(privateKeyXml[..100] ...); // 打印前100字符 Console.WriteLine(\n 公钥 (PEM) ); Console.WriteLine(publicKeyPem); // 3. 将密钥保存到文件实际项目应妥善保管私钥 File.WriteAllText(private_key.pem, privateKeyPem); File.WriteAllText(public_key.pem, publicKeyPem); File.WriteAllText(private_key.xml, privateKeyXml); Console.WriteLine(\n密钥已保存至文件。); } } } }运行这段代码你会在bin目录下得到三个文件。打开public_key.pem你会看到类似这样的内容-----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEAwV2l...Base64编码数据...AwIDAQAB -----END RSA PUBLIC KEY-----而private_key.xml则是一大段XML里面包含了Modulus、Exponent、D、P、Q、DP、DQ、InverseQ等所有RSA参数。重要安全警告私钥尤其是private_key.pem和private_key.xml是最高机密绝不能提交到代码仓库如Git、存放在客户端或通过不安全的渠道传输。在生产环境中私钥通常存放在服务器的密钥保管库如Azure Key Vault、AWS KMS、硬件安全模块HSM或受严格权限控制的配置文件中。3.2 使用公钥加密数据现在假设我们有一个独立的“发送方”程序它只拥有公钥需要加密一段信息。// 发送方 - 仅持有公钥 static byte[] EncryptWithPublicKey(string publicKeyPem, string plainText) { byte[] plainData Encoding.UTF8.GetBytes(plainText); using (RSA rsa RSA.Create()) { // 从PEM格式字符串导入公钥 rsa.ImportFromPem(publicKeyPem.ToCharArray()); // 使用OAEP with SHA-256填充进行加密推荐 // RSA加密有长度限制加密数据长度 (密钥长度/8) - 填充开销 // 对于2048位密钥和OAEP SHA-256最大明文长度约为 256 - 66 190字节 if (plainData.Length 190) { throw new ArgumentException($明文数据过长。对于2048位RSA-OAEP最大支持约190字节当前为{plainData.Length}字节。); } byte[] encryptedData rsa.Encrypt(plainData, RSAEncryptionPadding.OaepSHA256); return encryptedData; } }3.3 使用私钥解密数据接收方持有私钥负责解密。// 接收方 - 持有私钥 static string DecryptWithPrivateKey(string privateKeyPem, byte[] encryptedData) { using (RSA rsa RSA.Create()) { // 从PEM格式字符串导入私钥 rsa.ImportFromPem(privateKeyPem.ToCharArray()); // 使用相同的填充模式解密 byte[] decryptedData rsa.Decrypt(encryptedData, RSAEncryptionPadding.OaepSHA256); return Encoding.UTF8.GetString(decryptedData); } }3.4 串联测试在主函数中我们可以模拟这个流程// 模拟发送方 string originalMessage 这是一条需要加密的敏感信息比如交易流水号TX20231027001; string publicKey File.ReadAllText(public_key.pem); byte[] cipherData EncryptWithPublicKey(publicKey, originalMessage); Console.WriteLine($加密后的数据 (Base64): {Convert.ToBase64String(cipherData)}); // 模拟接收方 string privateKey File.ReadAllText(private_key.pem); string decryptedMessage DecryptWithPrivateKey(privateKey, cipherData); Console.WriteLine($解密后的消息: {decryptedMessage}); Console.WriteLine($解密是否成功: {originalMessage decryptedMessage});如果一切顺利你会看到加密后的密文一串Base64字符串以及成功解密还原的原文。4. 密钥格式转换解决跨平台对接的核心难题如果你只需要在纯C#环境内加解密那么用上面的ExportRSAPrivateKeyPem和ImportFromPem就足够了。但现实很骨感你经常需要和Java、Python、Node.js甚至硬件设备交互它们使用的密钥格式五花八门。这时格式转换就成了刚需。4.1 常见的密钥格式XML.NET Framework的“祖传”格式包含所有参数人类可读但冗长。PEM最常见的文本格式以-----BEGIN XXX-----和-----END XXX-----包裹Base64数据。它本身是一个容器里面可以装不同的数据格式。PKCS#1传统的RSA密钥标准PEM标签通常是BEGIN RSA PRIVATE KEY。PKCS#8更通用的私钥标准可以封装多种算法包括RSA标签是BEGIN PRIVATE KEY不包含算法标识或BEGIN ENCRYPTED PRIVATE KEY加密的。.NET 5 默认导出的PEM私钥是PKCS#1格式但可以通过ExportPkcs8PrivateKeyPem导出PKCS#8。DER上述所有格式的二进制版本是直接编码的ASN.1结构。4.2 使用BouncyCastle进行高级转换虽然.NET Core/5 在PEM支持上有了巨大进步但对于一些复杂的转换比如从PKCS#1到PKCS#8或者处理加密的PEM或者你还在使用.NET Framework那么BouncyCastle这个强大的第三方加密库几乎是必不可少的。它提供了极其丰富的格式支持和底层操作能力。首先通过NuGet安装Install-Package BouncyCastle.Cryptography或dotnet add package BouncyCastle.Cryptography。下面是一个功能更全面的转换工具类它解决了我在实际对接Java Spring Boot服务时遇到的具体问题using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Security; using System.Security.Cryptography; using System.Text; public class RsaKeyConverter { /// summary /// 将.NET RSA实例中的私钥转换为PKCS#8格式的PEM字符串无密码加密。 /// 此格式被Java的PKCS8EncodedKeySpec广泛支持。 /// /summary public static string ExportPrivateKeyToPkcs8Pem(RSA rsa) { RSAParameters rsaParams rsa.ExportParameters(true); RsaPrivateCrtKeyParameters privateKeyParam new RsaPrivateCrtKeyParameters( new Org.BouncyCastle.Math.BigInteger(1, rsaParams.Modulus), new Org.BouncyCastle.Math.BigInteger(1, rsaParams.Exponent), new Org.BouncyCastle.Math.BigInteger(1, rsaParams.D), new Org.BouncyCastle.Math.BigInteger(1, rsaParams.P), new Org.BouncyCastle.Math.BigInteger(1, rsaParams.Q), new Org.BouncyCastle.Math.BigInteger(1, rsaParams.DP), new Org.BouncyCastle.Math.BigInteger(1, rsaParams.DQ), new Org.BouncyCastle.Math.BigInteger(1, rsaParams.InverseQ) ); using (StringWriter writer new StringWriter()) { PemWriter pemWriter new PemWriter(writer); pemWriter.WriteObject(privateKeyParam); pemWriter.Writer.Flush(); return writer.ToString(); // 输出为 PKCS#1 PEM } // 注意上述代码实际输出PKCS#1。要得到PKCS#8需要稍复杂的ASN.1序列化。 // 更简单的方式是直接使用.NET 5的 ExportPkcs8PrivateKeyPem()。 } /// summary /// 将PKCS#8 PEM格式的私钥Java常见格式加载到.NET的RSA实例中。 /// 这个方法能处理“BEGIN PRIVATE KEY”标签。 /// /summary public static RSA ImportPrivateKeyFromPkcs8Pem(string pemString) { using (StringReader reader new StringReader(pemString)) { PemReader pemReader new PemReader(reader); object keyObject pemReader.ReadObject(); RsaPrivateCrtKeyParameters privateKeyParams; if (keyObject is AsymmetricCipherKeyPair keyPair) { // 如果PEM包含的是密钥对BEGIN RSA PRIVATE KEY privateKeyParams (RsaPrivateCrtKeyParameters)keyPair.Private; } else if (keyObject is RsaPrivateCrtKeyParameters rsaPrivate) { // 如果直接读出的就是私钥参数 privateKeyParams rsaPrivate; } else { throw new InvalidOperationException(不支持的PEM格式或不是RSA私钥。); } RSAParameters rsaParams new RSAParameters { Modulus privateKeyParams.Modulus.ToByteArrayUnsigned(), Exponent privateKeyParams.PublicExponent.ToByteArrayUnsigned(), D privateKeyParams.Exponent.ToByteArrayUnsigned(), P privateKeyParams.P.ToByteArrayUnsigned(), Q privateKeyParams.Q.ToByteArrayUnsigned(), DP privateKeyParams.DP.ToByteArrayUnsigned(), DQ privateKeyParams.DQ.ToByteArrayUnsigned(), InverseQ privateKeyParams.QInv.ToByteArrayUnsigned() }; RSA rsa RSA.Create(); rsa.ImportParameters(rsaParams); return rsa; } } /// summary /// 从标准的“BEGIN PUBLIC KEY” PEM格式导入公钥。 /// 这种格式是SubjectPublicKeyInfo结构Java的X509EncodedKeySpec使用它。 /// /summary public static RSA ImportPublicKeyFromX509Pem(string pemString) { using (StringReader reader new StringReader(pemString)) { PemReader pemReader new PemReader(reader); object keyObject pemReader.ReadObject(); RsaKeyParameters publicKeyParams; if (keyObject is RsaKeyParameters rsaKey) { publicKeyParams rsaKey; } else if (keyObject is AsymmetricCipherKeyPair keyPair) { // 有些PEM可能错误地包含了密钥对但我们需要公钥 publicKeyParams (RsaKeyParameters)keyPair.Public; } else { throw new InvalidOperationException(不支持的PEM格式或不是RSA公钥。); } RSAParameters rsaParams new RSAParameters { Modulus publicKeyParams.Modulus.ToByteArrayUnsigned(), Exponent publicKeyParams.Exponent.ToByteArrayUnsigned() }; RSA rsa RSA.Create(); rsa.ImportParameters(rsaParams); return rsa; } } }踩坑实录我曾经对接一个Java服务对方发来的公钥PEM标签是BEGIN PUBLIC KEY。我直接用.NET 6的ImportFromPem死活报错。后来发现ImportFromPem对标签格式比较挑剔。而用BouncyCastle的PemReader可以更灵活地解析。关键是要弄清楚对方给你的到底是什么格式。一个快速判断的方法是BEGIN RSA PUBLIC KEY通常是PKCS#1格式而BEGIN PUBLIC KEY是X.509/SubjectPublicKeyInfo (PKCS#8公钥) 格式。Java的KeyPairGenerator默认生成的是后者。5. 应对长数据分段加密与混合加密模式RSA算法有一个硬性限制它不能直接加密超过密钥长度的数据。对于2048位256字节的密钥使用OAEP填充后最大加密明文长度可能只有190字节左右。这显然无法满足加密长文本、文件等需求。解决这个问题有两种主流方案。5.1 分段加密与解密思路很简单把长数据切成多个符合长度限制的小块分别加密再拼接起来。解密时反向操作。这种方法虽然直观但效率低且密文会膨胀因为每个小块加密后长度都等于密钥长度。public static byte[] RsaEncryptLongData(RSA rsa, byte[] data, RSAEncryptionPadding padding) { int keySize rsa.KeySize / 8; // 密钥字节长度 int maxBlockSize padding RSAEncryptionPadding.Pkcs1 ? keySize - 11 : keySize - 42; // 粗略估算OAEP开销更大 // 更准确的做法应根据填充模式计算OAEP SHA-256 开销约66字节 using (MemoryStream msOutput new MemoryStream()) { for (int offset 0; offset data.Length; offset maxBlockSize) { int blockSize Math.Min(maxBlockSize, data.Length - offset); byte[] block new byte[blockSize]; Buffer.BlockCopy(data, offset, block, 0, blockSize); byte[] encryptedBlock rsa.Encrypt(block, padding); msOutput.Write(encryptedBlock, 0, encryptedBlock.Length); } return msOutput.ToArray(); } } public static byte[] RsaDecryptLongData(RSA rsa, byte[] encryptedData, RSAEncryptionPadding padding) { int keySize rsa.KeySize / 8; int blockSize keySize; // 密文块大小固定为密钥长度 using (MemoryStream msOutput new MemoryStream()) { for (int offset 0; offset encryptedData.Length; offset blockSize) { int currentBlockSize Math.Min(blockSize, encryptedData.Length - offset); byte[] block new byte[currentBlockSize]; Buffer.BlockCopy(encryptedData, offset, block, 0, currentBlockSize); byte[] decryptedBlock rsa.Decrypt(block, padding); msOutput.Write(decryptedBlock, 0, decryptedBlock.Length); } return msOutput.ToArray(); } }重要警告RSA标准本身并不直接定义或推荐这种分段加密模式。它破坏了密文的完整性且需要自己处理分块边界容易出错。在实践中更通用、更安全的方案是下面要讲的“混合加密”。5.2 混合加密RSA AES推荐方案这是业界标准的做法结合了非对称加密和对称加密的优点随机生成一个一次性的对称密钥比如AES-256的密钥。使用这个对称密钥用AES算法加密你的长数据。AES加密速度快适合大数据量。使用接收方的RSA公钥加密上一步生成的对称密钥。将加密后的对称密钥和AES加密后的数据一起发送给接收方。接收方用自己的RSA私钥解密出对称密钥再用对称密钥解密出原始数据。这样RSA只用于加密很小的对称密钥比如32字节完美避开了长度限制同时利用AES保证了大数据加密的性能。using System.Security.Cryptography; public class HybridEncryption { public static (byte[] encryptedSessionKey, byte[] iv, byte[] encryptedData) Encrypt(byte[] data, RSA rsaPublicKey) { // 1. 生成随机的AES密钥和初始化向量(IV) using (Aes aes Aes.Create()) { aes.KeySize 256; // AES-256 aes.GenerateKey(); aes.GenerateIV(); byte[] sessionKey aes.Key; byte[] iv aes.IV; // 2. 用AES加密数据 byte[] encryptedData; using (ICryptoTransform encryptor aes.CreateEncryptor()) { encryptedData encryptor.TransformFinalBlock(data, 0, data.Length); } // 3. 用RSA公钥加密AES密钥 byte[] encryptedSessionKey rsaPublicKey.Encrypt(sessionKey, RSAEncryptionPadding.OaepSha256); return (encryptedSessionKey, iv, encryptedData); } } public static byte[] Decrypt(byte[] encryptedSessionKey, byte[] iv, byte[] encryptedData, RSA rsaPrivateKey) { // 1. 用RSA私钥解密出AES密钥 byte[] sessionKey rsaPrivateKey.Decrypt(encryptedSessionKey, RSAEncryptionPadding.OaepSha256); // 2. 用解密出的AES密钥和IV解密数据 using (Aes aes Aes.Create()) { aes.Key sessionKey; aes.IV iv; using (ICryptoTransform decryptor aes.CreateDecryptor()) { return decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length); } } } }这种模式被广泛应用于TLS/SSL、PGP、S/MIME等安全协议中是处理任意长度数据加密的最佳实践。6. 实战问题排查与性能调优指南即使理解了所有原理在实际编码和运维中你依然会遇到各种稀奇古怪的问题。下面是我总结的一些常见“坑点”和解决思路。6.1 常见错误与排查清单错误现象可能原因排查步骤与解决方案“无效的数据”或“填充无效无法被移除”1.填充模式不匹配加密用OAEP解密用PKCS#1反之亦然。2.密钥不配对用A的公钥加密却试图用B的私钥解密。3.数据损坏或编码错误密文在传输或存储过程中被截断、修改或Base64解码出错。4.密钥格式错误导入的密钥字符串格式不对或者包含了多余字符如换行符、空格。1.首要检查确认加解密双方使用的RSAEncryptionPadding值完全一致。2.验证密钥对用已知的明文-密文对测试密钥是否正确配对。3.检查数据完整性确保接收到的密文与发送的完全一致。对于Base64使用Convert.FromBase64String并注意其是否可能抛出FormatException。4.清理密钥字符串使用.Trim()移除首尾空白确保PEM格式的标签头尾正确。“不支持指定的算法”1.平台不支持在旧版.NET Framework或特定环境下可能不支持某些填充模式如OaepSHA256。2.密钥参数错误导入的密钥参数如Modulus, Exponent字节数组格式不正确。1.降级填充模式尝试使用OaepSHA1或Pkcs1。2.检查密钥来源如果密钥来自外部系统确认其格式并用BouncyCastle等工具验证后再导入。“要加密的数据长度太长”明文数据长度超过了当前密钥和填充模式允许的最大值。1.检查长度计算最大允许长度。对于2048位OAEP明文应小于 ~190字节。2.采用混合加密对于长数据务必改用RSAAES的混合加密模式。“找不到公钥”或“密钥格式无效”1.PEM标签不匹配.ImportFromPem方法对标签格式有严格要求。2.密钥文件损坏文件不是有效的PEM或DER格式。1.使用BouncyCastle解析用PemReader代替ImportFromPem它更健壮。2.肉眼检查打开PEM文件确认-----BEGIN XXX-----和-----END XXX-----标签完整且正确。解密后得到乱码1.编码不一致加密时用UTF8.GetBytes解密后用ASCII.GetString。2.数据被意外修改可能在加密前或解密后对字节数组进行了不必要的转换。1.统一编码在整个流程中固定使用一种编码如UTF-8。2.逐字节比对在调试阶段输出并比对加密前和解密后的字节数组确保一致。6.2 性能考量与最佳实践RSA加解密是CPU密集型操作非常慢。以下是一些优化建议缓存RSA实例不要每次加解密都new RSACryptoServiceProvider()或RSA.Create()。生成或导入密钥后将RSA实例缓存起来注意线程安全。因为密钥生成和导入尤其是从XML/PEM字符串开销很大。区分使用场景加密使用公钥操作可以公开进行性能相对可接受。解密/签名使用私钥操作是性能瓶颈。应尽量减少调用频率。例如在Web API中不要用RSA解密每个请求的body而是用RSA解密一个会话密钥然后用更快的AES来解密后续请求。使用合适的密钥长度如前所述2048位是安全与性能的平衡点。非必要不使用更长密钥。考虑使用ECC对于新项目如果主要用途是数字签名而非加密可以考虑椭圆曲线加密ECC。它在相同安全强度下密钥更短、速度更快、资源消耗更少。.NET也提供了ECDsa类支持。6.3 一个真实的调试案例与Java服务对接我曾遇到一个需求我们的C#客户端需要调用一个Java Spring Boot服务的接口该接口要求用RSA公钥加密请求中的某个字段。Java后端给了我一个public.key文件。第一步检查密钥格式。用文本编辑器打开内容是-----BEGIN PUBLIC KEY-----开头。这是标准的X.509 SubjectPublicKeyInfo格式。第二步尝试直接导入。使用.NET 6的rsa.ImportFromPem(publicKeyText)失败提示格式错误。第三步使用BouncyCastle。改用上面RsaKeyConverter.ImportPublicKeyFromX509Pem方法成功导入。第四步加密并发送。用导入的公钥加密数据将Base64后的密文放在JSON请求体中发送。第五步Java端解密失败。Java后台报错。经过联调发现两个问题填充模式不匹配Java端默认使用的是RSA/ECB/PKCS1Padding而我C#端用的是OaepSHA256。将C#端改为使用RSAEncryptionPadding.Pkcs1后解决。Base64编码换行符Java端的Base64编码器默认会每76字符加一个换行而C#的Convert.ToBase64String默认不加。需要确保传输的密文字符串是“紧凑”的或者在解码时能处理换行符。最终确保两端在密钥格式、填充模式、编码方式上完全一致对接成功。这个案例充分说明了在跨系统加密通信中细节决定成败。