---
URL: "/zh-CN/blog/2025-09/API-Signature.html"
LLMS_URL: "/zh-CN/blog/2025-09/API-Signature.md"
lastUpdated: true
commentabled: true
recommended: true
title: "什么是API签名？"
description: "说说签名与验签"
date:

pageClass: "blog-page-class"
cover: "/covers/platform.svg"
---

---
lastUpdated: true
commentabled: true
recommended: true
title: 什么是API签名？
description: 说说签名与验签
date: 2025-09-28 09:00:00 
pageClass: blog-page-class
cover: /covers/platform.svg
---

## 简介 ##

*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 编码、用换行符连接不同部分）。此步骤至关重要，确保了【服务器】和【客户端】对“同一个请求”的【抽象表示】是完全一致的。

**创建待签字符串（String to Sign）**：

- 将规范化后的请求与其他元数据进行组合，形成一个待签名的最终字符串。其通用格式通常包含：

  - 使用的签名算法（如 `AWS4-HMAC-SHA256`）。
  - 请求的时间戳。
  - 其他凭证信息（如日期、区域、服务）。
  - 规范化请求的哈希值。

- 示例格式：`{算法}\n{时间戳}\n{credential-scope}\n{hashed-canonical-request}`

**计算签名（Calculate Signature）**：

- 使用分配的 `Secret Key`【发起方和接收方有共同的 Secret Key】，通过指定的加密算法（如 `HMAC-SHA256` ）对 *待签字符串* 进行加密计算，生成一个二进制的签名摘要，通常再将其转换为十六进制（Hexadecimal）字符串形式。

**组装并发送请求（Form and Send Request）**：

- 将生成的签名以及必要的验证信息（如 `Access Key ID`【注意，Access Key ID 就是用于说明发起方身份的东西】、时间戳、算法指示）添加到原始 HTTP 请求中。添加方式通常为：

  - 通过特殊的 HTTP 头（如 `Authorization Header`）。
  - 或作为查询字符串参数（如 `&signature=...&timestamp=...&accessKeyId=...`）。

- 最终将完整的请求发送至服务器。

### 第二阶段：服务端接收请求并验证 ###

**初步检查与信息提取**：

- 服务器接收到请求后，首先从请求头或查询参数中提取出 `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 是请求方和接收方共有的！】对请求核心要素进行哈希运算，生成一个唯一“指纹”（即签名）的安全机制，服务器通过核对这个“指纹”来验证请求者的合法身份并确保传输数据未被篡改。

## 🔒什么是HMAC ##

### 初印象 ###

**HMAC 怎么读？**

HMAC 的标准读法是：/ˈeɪtʃ.mæk/ （谐音：“艾奇-麦克”）

HMAC（Hash-based Message Authentication Code，基于【哈希】（注意，不是基于【加密】的！！）的消息认证码）是一种用于【验证消息完整性】和【真实性】的技术。

它结合了加密哈希函数【Cryptographic Hash Function】（如 SHA256、MD5）和一个秘密密钥（Secret Key）。

**简单来说，HMAC 可以确保**：

- 完整性：消息在传输过程中没有被篡改。
- 真实性：消息确实来自拥有共享秘密密钥的声称者。
- 它常用于 API 签名、JWT（JSON Web Tokens）、安全通信协议等场景。

### 工作流程 ###

**HMAC 的工作过程可以分为三个核心步骤**：

首先，发送方将【原始消息】与一个【共享的密钥】进行混合，通过特定的哈希算法（如 SHA-256）生成一段固定长度的【认证码（HMAC 值）】。这个过程中，密钥确保了生成结果的唯一性。

接着，发送方将【原始消息】和【计算出的 HMAC 值】一起传输给接收方。

最后，接收方使用相同的密钥和哈希算法对收到的消息重新计算 HMAC 值。通过比对计算出的 HMAC 与接收到的 HMAC 是否完全一致，来验证消息在传输过程中是否被篡改以及是否来自合法的发送方。任何对消息或密钥的修改都会导致验证失败。

这时候你可能会说，我管你这的那的，给我上图！！

ok，这就上图！！

```text
【发送方】
├─ 输入: [密钥] + [原始消息]
├─ 处理: HMAC = Hash(密钥, 消息)
└─ 发送: ┌─────────────┐
         │  原始消息    │
         │   HMAC值    │
         └─────────────┘
                 |
                 ↓ 【传输】→ (可能被篡改)
                 |
【接收方】        ↓
├─ 接收: ┌─────────────┐
│        │  收到消息    │
│        │  收到HMAC    │
│        └─────────────┘
├─ 处理: HMAC' = Hash(密钥, 收到消息)
├─ 比对: [HMAC'] vs [收到HMAC]
└─ 结果:
   ├─ 相同 → ✅ 验证成功 (消息完整可信)
   └─ 不同 → ❌ 验证失败 (消息被篡改)
```

### 关于 HMAC 值 ###

疑问：所以 HMAC 值总结来说算是一种摘要？还是签名？还是哈希值？还是其他的什么？

**可以这样来理解**：

HMAC 值本质上是一种“带密钥的哈希值”或“加密的摘要”，其目的是为了“签名认证”。

把它拆解开来就非常清晰了：

- 它是一种（哈希值/摘要）

  - 它的形态和普通的哈希值（如 SHA256）一样，是一段固定长度的、看起来是乱码的字符串。它确实是通过哈希函数计算出来的，具备哈希的核心特性：只要输入（消息或密钥）有丝毫改变，输出的 HMAC 值就会完全不同。

- 但它不是普通的哈希值，而是“带密钥的”

  - 普通的哈希值（例如 MD5、SHA1）是公开的，任何人用同一个算法对同一消息计算，得到的结果都相同。这意味着别人可以篡改消息后重新计算哈希值，让你无法察觉。
  - HMAC 的核心在于引入了“密钥”。没有这个密钥，就无法计算出相同的哈希结果。这使它从一种简单的完整性校验（检查数据是否出错）工具，升级为一种认证（Authentication）工具（确认数据来自谁且未被篡改）。

- 它的作用是“签名认证”，但不是数字签名

  - 目的类似签名：HMAC 用于证明消息的来源（拥有密钥的人）和完整性。从这个目的上看，它扮演了“签名”的角色，常被称为“消息认证码”。
  - 但技术上是两回事：真正的数字签名（如 RSA 签名）基于非对称加密（公钥/私钥），不需要共享密钥，还能提供不可否认性（用私钥签名后，拥有公钥的人都可以验证，但无法否认这个签名是自己做的）。
  - HMAC 基于对称加密（共享密钥），双方共享同一个密钥。接收方可以伪造出一个有效的 HMAC，因此无法提供不可否认性（因为双方都能计算出同样的 HMAC，无法确定到底是哪一方生成的）。

### 总结与类比 ###

| 特性        |      普通哈希值 (e.g., SHA256)      |  HMAC |  数字签名 (e.g., RSA) |
| :-----------: | :-----------: | :----: | :-----------: |
| 核心目的 | 无 | 完整性 + 身份认证 | 完整性 + 身份认证 + 不可否认性 |
| 密钥 | 灵活，支持复杂规则 | 对称密钥（一个秘密，双方共享） | 非对称密钥（私钥签名，公钥验证） |
| 输出 | 哈希值/摘要 | 带密钥的哈希值（消息认证码） | 数字签名 |
| 类比 | 文件封条（检查是否被拆） | 蜡封+私章（确认是谁封的） | 公证处的签名盖章（具有法律效力，无法抵赖） |

> 所以，最准确的描述是：*HMAC 值是一个用于认证目的的消息认证码，其技术实现形式是一个带密钥的哈希值。*

### 补充：签名和摘要的区别 ###

摘要是对消息本身计算出的、用于验证完整性的唯一指纹

签名则是使用非对称加密中的私钥对摘要进行加密后的结果，它在验证完整性的基础上，额外提供了身份认证和不可否认性。因为我如果能用你的公钥对消息解密成功，那我就可以确定你是发送方，而不是第三方，这就是不可否认性！！

### 啥是 HMAC？ ###

HMAC（基于哈希的消息认证码）是一种使用加密哈希函数（如 SHA-256）和共享密钥，为消息生成一段固定长度认证码的技术。

它用来同时验证数据的完整性（未被篡改）和真实性（来自拥有正确密钥的发送方）。

接收方使用相同密钥和算法重新计算认证码并进行比对，任何不一致都意味着消息无效。

### 签名简介 ###

【签名】用于验证消息来源和完整性。需配合哈希算法（如 SHA-256）和密钥对（用私钥签名，用公钥验证）。
它只用于非对称加密（也就是发送方和接收方分别有自己的公钥和私钥）

**签名和验签的流程【这是最重要的部分，要十分熟练】**

**发送方（签名）**：

a. 对原始消息计算哈希值（摘要）。

b. 使用发送方自己的私钥，对这个哈希值进行签名运算，生成数字签名。

c. 将【原始消息】和【数字签名】一起发送给接收方。

**接收方（验签）**：

a. 接收后，将收到的内容分离成【原始消息】和【数字签名】两部分。

b. 使用发送方的公钥对数字签名进行【验签】运算，从而解出一个哈希值 H1。【⚠️⚠️ 注意，这边说的是验签，这个词会比“解密”更精准】

c. 最关键的一步： 使用【相同的哈希算法】，对收到的原始消息单独计算一次哈希值 H2。【⚠️⚠️ 注意，也就是双方的哈希算法是提前约定好的】

d. 将计算得到的 H2 与从签名中解出的 H1 进行比对。

如果 H1 == H2：验签成功！证明：1. 消息确实来自发送方（认证性）。2. 消息在传输中未被篡改（完整性）。

如果 H1 != H2：验签失败！证明消息要么被篡改，要么不是声称的发送方所发。
