RSA客户端与服务端公钥交换实现及加密解密报错排查
问题1 解密失败与CryptographicException异常修复
根因分析
RsaEncryption类中RSACryptoServiceProvider csp被声明为静态变量,所有实例共享同一个csp对象。Encrypt方法中执行csp = new RSACryptoServiceProvider()会直接覆盖全局静态csp,导致原本存储的私钥参数失效,解密时导入私钥就会抛出「Key not valid for use in specified state」异常。- 服务端
HelloController每次接收请求都会实例化新的RsaEncryption对象,等于每次请求都会生成全新的公私钥对,客户端的RsaEncryption实例持有的是自己生成的完全不同的密钥对,解密必然失败。 - 加密逻辑逻辑不符合非对称加密规则:如果要让服务端加密的内容只有客户端能解密,应该用客户端的公钥加密,而不是服务端自己的公钥加密。用服务端自己的公钥加密的内容,只有服务端自己的私钥能解密,客户端当然解不开。
代码修复
首先修改Rsa加解密类,移除静态csp,支持导入外部公钥:
namespace HttpClientSample { public class RsaEncryption { // 移除静态csp,每个实例持有自己的密钥参数 private readonly RSAParameters _privateKey; private readonly RSAParameters _publicKey; // 无参构造:生成新的公私钥对(生成密钥对时用) public RsaEncryption() { using var csp = new RSACryptoServiceProvider(2048); _privateKey = csp.ExportParameters(true); _publicKey = csp.ExportParameters(false); } // 带参构造:导入外部公钥(导入对方公钥时用) public RsaEncryption(string publicKeyXml) { using var sr = new StringReader(publicKeyXml); var xs = new XmlSerializer(typeof(RSAParameters)); _publicKey = (RSAParameters)xs.Deserialize(sr); } public string GetPublicKeyXml() { using var sw = new StringWriter(); var xs = new XmlSerializer(typeof(RSAParameters)); xs.Serialize(sw, _publicKey); return sw.ToString(); } // 用当前实例持有的公钥加密 public string Encrypt(string plainText) { using var csp = new RSACryptoServiceProvider(); csp.ImportParameters(_publicKey); var data = Encoding.UTF8.GetBytes(plainText); // 统一用UTF8避免编码问题 var cipher = csp.Encrypt(data, true); // OAEP填充安全性更高,填false对应PKCS#1 v1.5 return Convert.ToBase64String(cipher); } // 用当前实例持有的私钥解密 public string Decrypt(string cipherText) { using var csp = new RSACryptoServiceProvider(); csp.ImportParameters(_privateKey); var dataBytes = Convert.FromBase64String(cipherText); var plainText = csp.Decrypt(dataBytes, true); return Encoding.UTF8.GetString(plainText); } } }
服务端修改,全局单例持有Rsa实例,不要每次请求生成新密钥,新增公钥获取接口:
public class HelloController : ApiController { // 全局单例,服务端启动时生成一次密钥对即可 private static readonly RsaEncryption _serverRsa = new RsaEncryption(); [HttpGet] [Route("api/publickey")] public string GetPublicKey() { return _serverRsa.GetPublicKeyXml(); } public string Get() { string ret = "Hello World"; // 加密需要用客户端公钥,验签需要用服务端私钥,根据业务场景选择 return ret; } }
客户端修改,启动时先初始化公钥:
// 客户端全局Rsa实例,启动时初始化 private static RsaEncryption _clientRsa; // 客户端启动时先调用该方法获取服务端公钥 static async Task InitRsaAsync() { var publicKey = await client.GetStringAsync("api/publickey"); _clientRsa = new RsaEncryption(publicKey); } static async Task<string> GetProductAsync(string path) { string product = null; HttpResponseMessage response = await client.GetAsync(path); if (response.IsSuccessStatusCode) { product = await response.Content.ReadAsStringAsync(); // 解密业务内容逻辑根据加密规则调整即可 } return product; }
问题2 RSA公钥交换原理与落地方案
核心原理
RSA是非对称加密算法,密钥成对出现:
- 公钥:可以公开分发,任何人都能拿到,用于加密内容或者验证私钥生成的签名
- 私钥:必须严格保密,只有持有者知道,用于解密对应公钥加密的内容或者生成签名
实现服务端向客户端传输加密内容,两种常用场景的逻辑如下:
| 场景 | 加密逻辑 | 密钥持有逻辑 |
|---|---|---|
| 内容只有指定客户端能解密 | 服务端用客户端的公钥加密内容 | 客户端自己生成公私钥,提前把自己的公钥同步给服务端 |
| 内容要保证是官方服务端发出(防篡改、防伪造) | 服务端用自己的私钥签名内容,客户端用服务端公钥验签,内容本身用对称加密(AES)加密 | 服务端生成公私钥,客户端提前持有服务端公钥 |
注意:RSA加密有长度限制,2048位RSA最多加密245字节左右的内容,不要直接用RSA加密业务内容,推荐用混合加密方案:生成随机AES密钥,用AES加密业务内容,再用对方的公钥加密AES密钥,最后把加密后的AES密钥和密文一起发给对方,对方用自己的私钥解密得到AES密钥,再用AES密钥解密密文。
公钥交换落地方案
方案1:预置公钥(最安全,推荐)
服务端提前生成好固定的公私钥对,把公钥XML/PEM格式直接预置到客户端的安装包/代码里,不需要线上传输公钥,完全避免中间人攻击。
方案2:首次请求拉取公钥(灵活,适用非敏感场景)
客户端首次启动时请求服务端的公钥接口拉取公钥,存在本地,后续请求直接用本地存储的公钥。该方案如果没有额外校验,可能被中间人攻击替换公钥,敏感业务不要使用。
方案3:直接使用HTTPS
如果是公网服务,直接用CA颁发的HTTPS证书即可,HTTPS底层已经实现了完整的非对称加密、公钥校验、混合加密逻辑,不需要自行实现RSA加解密。
内容的提问来源于stack exchange,提问作者sshh
相关产品推荐
相关产品推荐

