签名计算必须用确定性排序的参数,即所有参与签名的参数按key字典序升序排列并URL编码value;密钥须通过Azure Key Vault等安全方式注入且缓存;时间戳校验偏差不超过300秒并拒绝未来时间;.NET中需启用Buffering并重置Body位置。

签名计算必须用确定性排序的参数
API签名的核心是让客户端和服务端对同一请求生成完全一致的签名值,而实际中最常踩的坑就是参数顺序不一致。HTTP查询参数本身无序,Dictionary<string, string> 或 HttpRequest.Query 直接遍历的结果在不同运行时或 .NET 版本下可能顺序不同。
实操建议:
- 所有参与签名的参数(包括 URL 查询参数、JSON Body 中的字段、Header 中指定的固定字段如
X-Timestamp)必须先提取为键值对,再按 key 字典序升序 排序 - 拼接时统一用
key1=value1&key2=value2格式,不带空格、不 URL 编码 key,但 value 必须严格 URL 编码(用Uri.EscapeDataString(value),不是HttpUtility.UrlEncode) - Body 参与签名时,只取原始
Request.Body的字节流(需提前启用AllowSynchronousIO = true或用ReadAsStringAsync()后 SHA256),避免反序列化后再拼字符串——对象属性顺序不可控
密钥不能硬编码或走配置文件明文存储
签名验证依赖服务端持有的密钥(如 app_secret),一旦泄露,攻击者可伪造任意合法请求。常见错误是把密钥写死在 appsettings.json 里,或用 IConfiguration["App:Secret"] 直接读取。
实操建议:
- 密钥必须通过安全的外部源注入:Azure Key Vault、AWS Secrets Manager,或本地环境使用
dotnet user-secrets(仅开发) - 服务端验证签名时,密钥应缓存在内存中(如
IMemoryCache),避免每次请求都远程拉取 - 客户端传来的
app_id要先查库或缓存,拿到对应密钥;**禁止用app_id直接拼出密钥文件名或配置路径**(这是典型越权风险)
时间戳校验必须严格且带宽限制
签名中通常包含 timestamp 参数防重放,但很多人只做「是否为空」或「是否为数字」校验,导致攻击者可反复发送旧请求。
实操建议:
- 服务端收到请求后,立即获取当前 UTC 时间(
DateTimeOffset.UtcNow),与客户端传的timestamp做差值,**允许偏差不超过 300 秒(5 分钟)** - 必须拒绝
timestamp大于服务端当前时间的请求(防止客户端时钟快导致未来签名被提前接受) - 记录已处理过的
nonce(如果用了)或app_id + timestamp + signature组合,缓存 5 分钟,防止同一秒内重复提交(注意并发和分布式场景用 Redis 实现去重)
.NET 6+ 中拦截签名验证要避开中间件陷阱
有人习惯在 UseMiddleware<SignatureValidationMiddleware>() 里读取 Request.Body,结果后续 Controller 拿不到 Body —— 因为 Stream 已被读完且未重置。
实操建议:
- 在中间件开头调用
context.Request.EnableBuffering()(.NET 5+),然后用await context.Request.Body.ReadAsync(...)读取并保存原始字节 - 验证失败直接
context.Response.StatusCode = 401并return,**不要 throw 异常**(会触发全局异常页,掩盖真实问题) - 验证通过后,必须调用
context.Request.Body.Position = 0,否则 MVC 模型绑定拿不到数据;若 Body 是 JSON,建议额外加个if (contentType.Contains("json")) context.Request.ContentType = "application/json"防止 MIME 类型丢失


















