在分布式系统与开放API场景中,接口数据被篡改是高频安全问题。攻击者可能在中途修改订单金额、用户身份或业务状态,若服务端只信任收到的字段,就会造成资损或越权。RSA签名与验签利用非对称加密特性,让请求方用私钥对数据摘要签名,服务端用公钥验证,从而确认数据未被改动且来自合法调用者。

一、RSA签名与验签的基本原理
RSA是非对称加密算法,拥有一对密钥:私钥由签名方保密,公钥可公开给验签方。签名时并不是对整段业务数据加密,而是先通过哈希算法(如SHA256)把数据压缩成固定长度摘要,再用私钥对摘要做加密运算,得到签名值。这样做既保护了性能,也避免了明文暴露。
验签过程则相反:接收方用相同规则对原始数据计算摘要,并用公钥对签名值解密,比对两个解密后的摘要是否一致。若一致,说明数据完整且由持有私钥的一方发出;若不一致,则代表数据被篡改或签名伪造。需要注意,RSA签名保护的是完整性和来源可信,不负责机密性,敏感明文仍需配合传输层加密。
1.1 为什么不能只用对称加密或HTTPS
HTTPS能防链路嗅探,但无法防止服务端内部逻辑被绕过、或合法客户端本身发起的重放攻击。对称加密要求双方持有相同密钥,一旦某一方泄露,所有调用方都会失控。RSA把签名权与验证权分离,更适合多端调用、中心化校验的接口架构。
- 私钥签名:仅落在可信服务或网关,降低暴露面
- 公钥验签:可下发到任意消费端,无需保密
- 防篡改:摘要比对让任何字节变化都导致验签失败
二、C#生成RSA密钥对
在C#中,可以使用System.Security.Cryptography.RSA类来创建密钥。下面示例生成2048位密钥,并导出XML格式以便存储。实际项目中私钥应放在受保护配置或密钥管理服务中,不要硬编码进代码仓库。
using System;
using System.Security.Cryptography;
class KeyGenerator
{
static void Main()
{
using (RSA rsa = RSA.Create(2048))
{
// 导出包含私钥的XML,用于签名服务
string privateKeyXml = rsa.ToXmlString(true);
// 导出仅含公钥的XML,用于验签服务
string publicKeyXml = rsa.ToXmlString(false);
Console.WriteLine("私钥XML:");
Console.WriteLine(privateKeyXml);
Console.WriteLine("公钥XML:");
Console.WriteLine(publicKeyXml);
}
}
}
上述代码使用RSA.Create(2048)明确指定密钥长度,比默认长度更可控。私钥XML中含有Modulus、Exponent、P、Q等参数,必须加密保存;公钥XML只含Modulus与Exponent,可随接口文档公开。
若你使用.NET Core或.NET 5+,也可以导出PEM格式,但在旧版Web API中XML兼容性更好。无论哪种格式,都要保证私钥不进入前端包体,否则签名机制形同虚设。
三、C#实现RSA签名
签名方需要对待发送数据排序并拼接,计算SHA256摘要后调用RSA.SignData方法。以下示例把业务参数按key排序,组成query串,再用私钥签名,最后把签名做Base64编码放入请求头。
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
class RsaSigner
{
// 用私钥XML初始化RSA
static RSA GetRsaFromPrivateXml(string xml)
{
RSA rsa = RSA.Create();
rsa.FromXmlString(xml);
return rsa;
}
// 对参数字典签名,返回Base64签名
public static string Sign(Dictionary<string, string> dict, string privateKeyXml)
{
// 按key排序,保证验签方拼接顺序一致
var sorted = new SortedDictionary<string, string>(dict);
StringBuilder sb = new StringBuilder();
foreach (var kv in sorted)
{
sb.Append(kv.Key).Append('=').Append(kv.Value).Append('&');
}
string raw = sb.ToString().TrimEnd('&');
using (RSA rsa = GetRsaFromPrivateXml(privateKeyXml))
{
byte[] data = Encoding.UTF8.GetBytes(raw);
// 使用SHA256哈希和RSA签名
byte[] signBytes = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signBytes);
}
}
}
代码中SortedDictionary保证参数顺序稳定,这是防篡改的关键细节。如果签名方和验签方拼接顺序不同,即使数据没被改,摘要也会不一致。选择RSASignaturePadding.Pkcs1是兼容大多数旧系统的方案,新项目也可评估Pss填充提升安全性。
Base64后的签名建议放在自定义请求头如X-Signature中,和业务参数分离,便于网关统一拦截。签名内容应为业务关键字段,而非整个HTTP体,以减少不必要的计算开销。
四、C#实现RSA验签
验签服务拿到请求参数与签名后,用相同拼接规则重现原始串,并用公钥验证签名。下面给出完整验签方法,返回布尔值表示是否通过。
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
class RsaVerifier
{
static RSA GetRsaFromPublicXml(string xml)
{
RSA rsa = RSA.Create();
rsa.FromXmlString(xml);
return rsa;
}
public static bool Verify(Dictionary<string, string> dict, string signatureBase64, string publicKeyXml)
{
var sorted = new SortedDictionary<string, string>(dict);
StringBuilder sb = new StringBuilder();
foreach (var kv in sorted)
{
sb.Append(kv.Key).Append('=').Append(kv.Value).Append('&');
}
string raw = sb.ToString().TrimEnd('&');
using (RSA rsa = GetRsaFromPublicXml(publicKeyXml))
{
byte[] data = Encoding.UTF8.GetBytes(raw);
byte[] signBytes = Convert.FromBase64String(signatureBase64);
return rsa.VerifyData(data, signBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
}
验签失败时要直接拒绝请求,并返回统一错误码,避免把具体原因暴露给调用方。生产环境还应加上时间戳与随机数nonce,防止攻击者截获合法请求后重放。时间戳偏差一般控制在五分钟内,nonce则在服务端缓存去重。
当接口并发较高时,验签属于CPU密集型操作,建议把公钥验签放在独立线程池或网关层,避免阻塞业务处理。RSA 2048在普通服务器上单次验签约零点几毫秒,量极大时可考虑更换为Ed25519等更轻量算法,但RSA生态工具最全,仍是多数企业的首选。
五、防止接口数据篡改的补充策略
仅靠RSA签名还不够,完整防篡改方案应结合多重手段。下面用表格列出常见风险与对应策略。
| 风险类型 | 说明 | 应对方式 |
|---|---|---|
| 参数重放 | 攻击者复用旧签名请求 | 加timestamp与nonce,服务端校验并缓存 |
| 部分字段未签名 | 只签了金额,没签用户ID | 所有关键字段纳入签名拼接串 |
| 私钥泄露 | 签名服务被攻破 | 私钥存KMS,定期轮换公钥 |
| 哈希被碰撞 | 使用MD5或SHA1 | 统一使用SHA256及以上 |
在实践中,很多团队把签名逻辑封装成中间件,统一在请求进入控制器前完成验签。这样业务代码无需关心安全细节,也避免有人漏调验证方法。对于对外暴露的开放平台,还应提供SDK,让调用方少写拼接与编码代码,降低接入出错率。
最后提醒,RSA签名验证的是数据完整性,不是业务逻辑正确性。验签通过后,仍要对参数做范围校验、权限校验与幂等处理,才能构建真正稳健的接口安全体系。