前言
本文谈谈 【API 签名】 目标是看完之后,需要能用自己的语言说出【什么是 API 签名】
正文
API 签名(API Signature)是一种用于身份验证(Authentication) 和保证数据完整性(Data Integrity) 的安全机制。其核心是客户端使用密钥(Secret Key) ,通过密码学哈希运算【注意,这里是一种哈希】对请求的特定要素进行计算,生成一个唯一的签名(Signature)。
服务器通过验证此签名来确认请求者的身份【合法】且传输过程中数据【未被篡改】。
常用技术搭配:
- HMAC (Hash-based Message Authentication Code,如
HMAC-SHA256
):最常用的签名计算算法,运算速度快,安全性高。之前有写过文章,不了解 HMAC 的小伙伴可以去康康。 - 访问密钥对(Access Key Pair) :其中一个是Access Key ID (公开,用于标识身份),另一个是 Secret Key(机密,用于计算签名,绝不通过网络传输)。注意,不是非对称加密中的公钥和私钥!!!
- 时间戳(Timestamp):核心防重放机制,通常为 UTC 时间戳(Unix Epoch Time)。
- Nonce:一次性的随机数(Number used once),作为补充防重放机制,确保请求的唯一性。
补充内容:啥是重放攻击?🧐
重放攻击(Replay Attack)是一种网络攻击手段。
攻击者通过窃听或拦截客户端与服务器之间的正常通信,获取到一个【有效的】、【带有认证信息】(如密码、Token 或 API 签名)的数据请求包。然后,攻击者并不需要破解这个认证信息的内容,而是直接将它【原封不动地】、在稍后某个时间【重复发送】给服务器。
由于该请求包含合法的认证凭证,服务器无法区分这是来自真实客户的请求还是恶意重放的请求,从而被欺骗执行重复的操作,例如再次转账、重复下单或篡改数据。
补充内容:如何解决重放攻击?为了防止重放攻击,API 签名通常会结合时间戳(Timestamp)和Nonce(一次性随机数)等机制。
服务器会在接收到请求时,检查请求中的时间戳是否在合理的时间窗口内(如 10 分钟),并验证 Nonce 是否之前未被使用过。如果校验失败,服务器会拒绝处理该请求,认为它可能是一个重放攻击。
P.S. 后面还会提到时间戳防止重放攻击相关的内容 👀
API 签名的流程
补充说明:这边举的例子是属于比较【严格】的签名流程了。我目前见到的 API 签名流程没有这么复杂,仅对请求体进行签名操作。
第一阶段:客户端构造签名并发起请求
-
构建标准请求(Canonical Request):
- 客户端收集 HTTP 请求的必需元素,包括:HTTP 方法(如 GET、POST)、请求的 API 端点(Endpoint)、查询字符串参数(Query String)、需要参与签名的特定头(Headers,如
Host
、Content-Type
)、以及请求体(Body,如果是 POST 或 PUT 请求)。 - 将这些元素按照 API 提供商定义的特定规则进行**规范化(Canonicalization)**处理(例如:将参数按字母序排序、进行标准的 URI 编码、用换行符连接不同部分)。此步骤至关重要,确保了【服务器】和【客户端】对"同一个请求"的【抽象表示】是完全一致的。
- 客户端收集 HTTP 请求的必需元素,包括:HTTP 方法(如 GET、POST)、请求的 API 端点(Endpoint)、查询字符串参数(Query String)、需要参与签名的特定头(Headers,如
-
创建待签字符串(String to Sign):
- 将规范化后的请求与其他元数据进行组合,形成一个待签名的最终字符串。其通用格式通常包含:
- 使用的签名算法(如
AWS4-HMAC-SHA256
)。 - 请求的时间戳。
- 其他凭证信息(如日期、区域、服务)。
- 规范化请求的哈希值。
- 使用的签名算法(如
- 示例格式:
{算法}\n{时间戳}\n{credential-scope}\n{hashed-canonical-request}
- 将规范化后的请求与其他元数据进行组合,形成一个待签名的最终字符串。其通用格式通常包含:
-
计算签名(Calculate Signature):
- 使用分配的 Secret Key 【发起方和接收方有共同的 Secret Key】,通过指定的加密算法(如
HMAC-SHA256
)对待签字符串进行加密计算,生成一个二进制的签名摘要,通常再将其转换为十六进制(Hexadecimal)字符串形式。
- 使用分配的 Secret Key 【发起方和接收方有共同的 Secret Key】,通过指定的加密算法(如
-
组装并发送请求(Form and Send Request):
- 将生成的签名以及必要的验证信息(如
Access Key ID
【注意,Access Key ID 就是用于说明发起方身份的东西】、时间戳、算法指示)添加到原始 HTTP 请求中。添加方式通常为:- 通过特殊的 HTTP 头(如
Authorization
Header)。 - 或作为查询字符串参数(如
&signature=...×tamp=...&accessKeyId=...
)。
- 通过特殊的 HTTP 头(如
- 最终将完整的请求发送至服务器。
- 将生成的签名以及必要的验证信息(如
第二阶段:服务端接收请求并验证
-
初步检查与信息提取:
- 服务器接收到请求后,首先从请求头或查询参数中提取出
Access Key ID
【用于判断你是谁】、时间戳(Timestamp)、客户端签名(Client Signature)等信息。
- 服务器接收到请求后,首先从请求头或查询参数中提取出
-
时效性验证(防重放攻击关键步骤):
- 服务器系统获取当前时间,并与请求中的时间戳进行比较。
- 计算两者时间差的绝对值。如果该差值超过了预设的有效时间窗口(Validity Window) (例如 15 分钟),服务器立即判定该请求无效并拒绝,返回错误(如
403 Forbidden
或419 Authentication Timeout
)。此步骤有效防御了重放攻击(Replay Attack)。
-
获取对端密钥:
- 根据提取出的
Access Key ID
,服务器在自身的 credential 数据库或缓存中查找对应的 Secret Key。如果找不到,则身份验证失败。
- 根据提取出的
-
重构与计算签名(服务端):
- 服务器使用与客户端完全相同的规则 ,从接收到的原始请求中提取数据,重构出规范化请求 ,进而生成与服务端一致的待签字符串,也就是和发起方一样,重新抽象化此次请求,确保请求的任何一个细节都是与预期一致的。
- 使用查找到的 Secret Key 和相同的加密算法,对该待签字符串进行计算,得到服务端签名(Server-Side Signature)。
-
签名比对与最终验证:
- 将计算得到的服务端签名 与客户端传来的客户端签名进行安全的比对(通常采用【恒定时间比较算法】,以防止【计时攻击 ------ 后面我会补充计时攻击的知识】)。
- 只有满足以下两个条件,验证才算通过 :
- 条件一:时间戳在有效期内。
- 条件二:两个签名完全一致。
- 签名一致证明了请求者的身份合法(拥有正确的 Secret Key)且请求内容在传输过程中未被任何方式篡改。
补充知识:什么是【计时攻击】?
以 API 签名验证为例,一个不安全的字符串比较函数可能是这样的:它从第一个字符开始逐个比对,一旦发现不匹配就立即返回失败。那么,比较 abcde 和 accde 会比比较 abcde 和 xbcdе 花费更少的时间,因为前者在第二个字符就失败了,而后者在第一个字符就失败了。
攻击者可以利用这个微小的耗时差异,反复发送大量精心构造的请求并记录响应时间,从而像"猜数字"一样,逐个字符地破解出正确的签名。
因此,安全的系统会使用"恒定时间比较"函数(如 Python 的 secrets.compare_digest),确保无论匹配到第几个字符失败,比较操作所花费的时间都是相同的,从而彻底杜绝计时攻击。
我也是刚刚了解这一种攻击方式,说实话,这操作我已经惊呆了 🙀🙀🙀,黑客无孔不入啊 bro
- 处理与响应 :
- 验证通过:服务器正常处理请求,并返回业务数据。
- 验证失败:服务器立即终止处理,返回
4xx
状态码的错误响应(如403 Forbidden
)。
这时候可能会有小伙伴说:😠😠😠 我管你这的那的,给我上图!!上图!!
ok,对应的流程图如下!
text
【客户端 (Client)】
├─ 输入: [Secret Key] + [HTTP请求要素 (Method, Path, Headers, Body, Timestamp, Nonce...)]
├─ 处理:
│ 1. 规范化: 将请求要素按特定规则排序、编码,生成【规范化请求 (Canonical Request)】
│ 2. 组合: 将规范化请求与其他元数据组合,生成【待签字符串 (String to Sign)】
│ 3. 计算签名: Signature = HMAC-SHA256(Secret Key, String to Sign)
└─ 发送: ┌─────────────────────────────────────────────────┐
│ 原始的 HTTP 请求 (明文) │
│ ├─ Headers: │
│ │ ... │
│ │ Authorization: Credential={AccessKeyId}, │
│ │ SignedHeaders=..., │
│ │ Signature={Signature} │
│ │ X-Timestamp: {Timestamp} │
│ │ X-Nonce: {Nonce} │
│ └─ ... │
│ ├─ Body: {Request Body} │
└─────────────────────────────────────────────────┘
|
↓ 【网络传输】→ (可能被窃听、篡改、重放)
|
【服务端 (Server)】 ↓
├─ 接收: ┌─────────────────────────────────────────────────┐
│ │ 收到的 HTTP 请求 │
│ │ ├─ Headers: │
│ │ │ ... │
│ │ │ Authorization: ... │
│ │ │ X-Timestamp: {Received_Timestamp} │
│ │ │ X-Nonce: {Received_Nonce} │
│ │ └─ ... │
│ │ ├─ Body: {Received_Body} │
│ └─────────────────────────────────────────────────┘
├─ 处理:
│ 1. 📛 初步检查: 提取 AccessKeyId, Received_Timestamp, Received_Nonce, Signature
│ 2. ⏰ 时效验证: |Server_Time - Received_Timestamp| > TimeWindow? → ❌失败 (防重放)
│ 3. 🔑 获取密钥: 用 AccessKeyId 查数据库,找到对应的 Secret_Key
│ 4. 🛠️ 重构签名:
│ 1. 按相同规则从【收到请求】生成【规范化请求_Server】
│ 2. 组合生成【待签字符串_Server】
│ 3. Signature_Server = HMAC-SHA256(Secret_Key, String to Sign_Server)
│ 5. 🔍 安全比对: Compare(Signature_Server, Received_Signature)
├─ 验证结果:
│ ├─ ✅ 成功 (同时满足):
│ │ ├─ 时间戳在有效窗口内
│ │ └─ 签名比对完全一致
│ │ → 处理请求,返回业务数据 (200 OK)
│ │
│ └─ ❌ 失败 (任一不满足):
│ → 立即拒绝请求,返回错误 (如 403 Forbidden / 419 Timeout)
└─
最后
信息安全之路任重道远
最后来回答下开头的问题:什么是 API 签名?
API 签名是一种使用密钥(Secret Key)【这个 Secret Key 是请求方和接收方共有的!】对请求核心要素进行哈希运算,生成一个唯一"指纹"(即签名)的安全机制,服务器通过核对这个"指纹"来验证请求者的合法身份并确保传输数据未被篡改。