前言

在 Web 项目中,接口安全最基础的一层一定是 HTTPS。HTTPS 已经通过 TLS 解决了传输过程中的窃听、篡改和中间人攻击问题。对于大多数普通业务接口来说,HTTPS 加登录态认证已经足够。

但在一些敏感场景中,比如实名认证、银行卡绑定、支付确认、修改手机号、修改密码、隐私资料提交等,仅依赖 HTTPS 有时还不够。原因不是 HTTPS 不安全,而是业务系统里还可能存在日志落盘、代理转发、网关调试、内部链路暴露、前后端错误处理等额外风险。

这时可以在 HTTPS 之外,再对敏感业务数据做一层应用层加密。本文就从算法对比开始,介绍一套比较通用的接口业务数据加密传输方案。

常见加密算法对比

在设计接口加密方案之前,先要理解几类常见算法的特点。很多安全方案不是靠某一个算法完成的,而是多种算法各自负责一部分能力。

对称加密算法

对称加密的特点是:加密和解密使用同一把密钥。

常见算法有 AES、ChaCha20、SM4、DES、3DES 等。

算法 特点 缺点 建议
AES 标准成熟、性能好、应用最广 需要安全传递密钥;模式选错会有风险 推荐
AES-GCM 加密同时提供完整性校验,适合接口数据加密 IV 不能在同一密钥下重复使用 强烈推荐
ChaCha20-Poly1305 移动端性能好,对硬件 AES 加速依赖低 国内传统后端栈支持度可能不如 AES 推荐
SM4 国产商用密码算法,适合国密合规场景 非国密场景生态不如 AES 普遍 合规场景推荐
DES 历史算法,密钥太短 已不安全 不推荐
3DES 比 DES 稍强,但慢且老旧 已逐步淘汰 不推荐

业务数据加密通常选择:

1
AES-256-GCM

AES 负责加密正文,GCM 模式还能校验密文是否被篡改,比传统的 AES-CBC 更适合新项目。

非对称加密算法

非对称加密使用一对密钥:

1
2
公钥:可以公开,用于加密或验签
私钥:必须保密,用于解密或签名

常见算法有 RSA、ECC、SM2 等。

算法 特点 缺点 建议
RSA-OAEP 成熟、兼容性好,适合加密临时 AES 密钥 速度慢,不适合加密大数据 推荐
RSA PKCS#1 v1.5 老系统常见 安全性不如 OAEP,不建议新项目使用 谨慎使用
ECC / ECDH 密钥短、性能好,适合密钥协商 实现和兼容性比 RSA 更复杂 推荐给有经验团队
SM2 国密非对称算法 非国密生态兼容性有限 合规场景推荐

非对称加密不适合直接加密整个请求体,因为它慢,而且可加密的数据长度有限。更常见的方式是混合加密:

1
2
AES 加密业务数据
RSA-OAEP 加密 AES 密钥

这样既有 AES 的性能,也有 RSA 解决密钥传递问题的能力。

哈希摘要算法

哈希不是加密,因为哈希结果不能解密回原文。它的作用是生成数据指纹。

常见算法有 SHA-256、SHA-512、SM3、MD5、SHA1。

算法 特点 缺点 建议
SHA-256 通用、安全、支持广泛 只能做摘要,不能直接防伪造 推荐
SHA-512 摘要更长,适合部分服务端场景 输出更长,未必必要 推荐
SM3 国产摘要算法 非国密场景使用较少 合规场景推荐
MD5 速度快,历史项目常见 已不安全,容易碰撞 不推荐
SHA1 历史算法 已不安全 不推荐

注意:用户密码不要直接用 SHA-256、MD5 这类普通哈希保存。密码存储应使用 Argon2id、bcrypt 或 PBKDF2。

消息认证与签名算法

签名类算法主要用于防篡改和验身份。

常见方案有 HMAC-SHA256、RSA-SHA256、ECDSA、SM2 签名。

算法 特点 缺点 适合场景
HMAC-SHA256 简单、快、实现稳定 双方必须共享同一个密钥 内部接口、业务接口
RSA-SHA256 私钥签名、公钥验签,适合开放平台 性能比 HMAC 低 第三方接口、支付回调
ECDSA 签名短、性能好 实现细节要求高 现代证书与开放接口
SM2 签名 国密签名算法 非国密生态支持有限 国密合规项目

如果接口数据已经使用 AES-GCM 加密,GCM 本身就带认证能力,可以发现密文是否被篡改。若系统还需要统一签名规范,可以额外增加 HMAC-SHA256。

推荐方案

对于普通 Web 项目的敏感接口,推荐方案如下:

1
2
3
4
5
6
HTTPS
+ 登录 Token
+ AES-256-GCM 加密业务数据
+ RSA-OAEP 加密 AES 密钥
+ timestamp + nonce 防重放
+ 可选 HMAC-SHA256 签名

这套方案中,每个部分的职责很清晰:

组件 作用
HTTPS 保护传输通道
Token 识别当前用户身份
AES-256-GCM 加密请求体中的敏感业务数据
RSA-OAEP 安全传递本次请求使用的 AES 密钥
timestamp 限制请求有效时间
nonce 防止同一请求被重复提交
HMAC-SHA256 可选,用于统一接口签名和防篡改

整体思路是:不要把所有事情都交给一个算法。AES 负责快,RSA 负责安全传密钥,timestamp 和 nonce 负责防重放,签名负责防篡改。

下面这张图可以帮助理解各个组件之间的分工:

flowchart LR
    A[浏览器客户端] -->|HTTPS 请求| B[服务端网关]
    B --> C[业务服务]
    C --> D[(Redis nonce 缓存)]
    C --> E[(业务数据库)]

    subgraph Client[客户端侧]
        A1[生成 AES key]
        A2[AES-GCM 加密业务数据]
        A3[RSA-OAEP 加密 AES key]
        A4[生成 timestamp 和 nonce]
    end

    subgraph Server[服务端侧]
        S1[校验 Token]
        S2[校验 timestamp]
        S3[校验 nonce]
        S4[RSA 私钥解密 AES key]
        S5[AES-GCM 解密业务数据]
    end

    Client --> A
    B --> Server

请求结构设计

原始业务数据可能是这样:

1
2
3
4
5
{
"realName": "张三",
"idCard": "110101199001011234",
"phone": "13800138000"
}

加密后,接口真正提交的数据可以设计成:

1
2
3
4
5
6
7
8
{
"encryptedKey": "RSA-OAEP加密后的AES密钥",
"iv": "AES-GCM使用的初始化向量",
"data": "AES-GCM加密后的业务数据",
"timestamp": 1790611200000,
"nonce": "一次性随机字符串",
"sign": "可选的HMAC-SHA256签名"
}

其中:

  • encryptedKey:前端生成 AES 密钥后,用服务端 RSA 公钥加密得到。
  • iv:AES-GCM 加密使用的 IV,推荐 12 字节随机数。
  • data:业务 JSON 序列化后加密得到的密文。
  • timestamp:当前请求时间戳,用来限制请求有效期。
  • nonce:随机字符串,用来防止重复请求。
  • sign:可选字段,如果系统已有签名规范,可以把关键字段一起签名。

请求体的封装关系可以表示为:

flowchart TD
    P[原始业务 JSON] --> A[AES-GCM 加密]
    K[随机 AES key] --> A
    IV[随机 IV] --> A
    A --> DATA[加密后的 data]

    K --> R[RSA-OAEP 公钥加密]
    PUB[服务端 RSA 公钥] --> R
    R --> EK[encryptedKey]

    DATA --> REQ[接口请求体]
    EK --> REQ
    IV --> REQ
    T[timestamp] --> REQ
    N[nonce] --> REQ
    SIGN[可选 sign] --> REQ

前端加密流程

前端处理流程如下:

1
2
3
4
5
6
7
8
1. 准备原始业务 JSON
2. 生成随机 AES 密钥
3. 生成随机 IV
4. 使用 AES-GCM 加密业务 JSON
5. 使用服务端 RSA 公钥加密 AES 密钥
6. 生成 timestamp 和 nonce
7. 可选:计算 HMAC-SHA256 签名
8. 提交加密后的请求体

完整交互时序如下:

sequenceDiagram
    participant C as 前端
    participant S as 服务端
    participant R as Redis

    C->>C: 生成 AES key 和 IV
    C->>C: AES-GCM 加密业务 JSON
    C->>C: RSA-OAEP 加密 AES key
    C->>C: 生成 timestamp 和 nonce
    C->>S: 提交 encryptedKey、iv、data、timestamp、nonce、sign
    S->>S: 校验 Token 和 timestamp
    S->>R: 查询 nonce 是否已使用
    R-->>S: 返回查询结果
    S->>R: 写入 nonce,设置短 TTL
    S->>S: RSA 私钥解密 AES key
    S->>S: 校验签名并 AES-GCM 解密 data
    S-->>C: 返回业务处理结果

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const payload = {
realName: "张三",
idCard: "110101199001011234",
phone: "13800138000"
}

const aesKey = randomBytes(32) // AES-256
const iv = randomBytes(12) // AES-GCM 推荐 12 字节 IV

const data = aesGcmEncrypt(JSON.stringify(payload), aesKey, iv)
const encryptedKey = rsaOaepEncrypt(aesKey, serverPublicKey)

const timestamp = Date.now()
const nonce = randomString(32)

const signText = [
encryptedKey,
iv,
data,
timestamp,
nonce
].join(".")

const sign = hmacSha256(signText, aesKey)

await fetch("/api/sensitive/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${accessToken}`
},
body: JSON.stringify({
encryptedKey,
iv,
data,
timestamp,
nonce,
sign
})
})

实际开发时,浏览器端应使用 window.crypto.subtle 或成熟加密库,不要自己实现底层算法。

服务端解密流程

服务端收到请求后,不要马上解密业务数据,建议先做基础校验:

1
2
3
4
5
6
7
1. 校验登录态 Token
2. 校验 timestamp 是否在有效窗口内
3. 校验 nonce 是否已使用
4. 使用 RSA 私钥解密 AES 密钥
5. 可选:校验 HMAC-SHA256 签名
6. 使用 AES-GCM 解密业务数据
7. 执行业务逻辑

服务端校验顺序建议保持“先低成本拒绝,再做高成本解密”:

flowchart TD
    A[收到敏感接口请求] --> B{Token 是否有效}
    B -- 否 --> X1[拒绝请求]
    B -- 是 --> C{timestamp 是否过期}
    C -- 是 --> X2[拒绝请求]
    C -- 否 --> D{nonce 是否已使用}
    D -- 是 --> X3[拒绝请求]
    D -- 否 --> E[记录 nonce 并设置 TTL]
    E --> F[RSA 私钥解密 AES key]
    F --> G{签名是否正确}
    G -- 否 --> X4[拒绝请求]
    G -- 是 --> H[AES-GCM 解密 data]
    H --> I[执行业务逻辑]

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
async function handleSensitiveRequest(req) {
const {
encryptedKey,
iv,
data,
timestamp,
nonce,
sign
} = req.body

const user = await verifyAccessToken(req.headers.authorization)

if (Math.abs(Date.now() - timestamp) > 5 * 60 * 1000) {
throw new Error("请求已过期")
}

const nonceKey = `nonce:${user.id}:${nonce}`
const exists = await redis.exists(nonceKey)
if (exists) {
throw new Error("重复请求")
}
await redis.set(nonceKey, "1", "EX", 300)

const aesKey = rsaOaepDecrypt(encryptedKey, serverPrivateKey)

const signText = [
encryptedKey,
iv,
data,
timestamp,
nonce
].join(".")

const expectedSign = hmacSha256(signText, aesKey)
if (sign && !timingSafeEqual(sign, expectedSign)) {
throw new Error("签名错误")
}

const json = aesGcmDecrypt(data, aesKey, iv)
const payload = JSON.parse(json)

return saveSensitiveInfo(user.id, payload)
}

这里有一个细节:比较签名时最好使用恒定时间比较函数,例如 Node.js 里的 crypto.timingSafeEqual,避免极端情况下的时序攻击。

IV 和 nonce 的区别

IV 和 nonce 都是随机值,但它们不是同一个东西。

字段 用途 谁使用 是否需要存储
IV 给 AES-GCM 加密和解密使用 加密算法 不需要长期存储,但请求中要传给服务端
nonce 防止请求被重复提交 业务服务 需要短期存入 Redis 等缓存

简单理解:

1
2
IV:加密算法用的随机数
nonce:业务防重放用的一次性随机串

不要为了省字段把二者混用。它们的职责、生命周期和校验逻辑都不同。

nonce 由谁生成

nonce 可以由前端生成,也可以由后端生成。

前端生成 nonce

这是大多数普通敏感接口的做法:

1
2
3
4
前端生成 nonce
请求时带给后端
后端检查 nonce 是否已使用
未使用则写入 Redis,并设置短过期时间

优点是简单,不需要多一次请求。

适合:

  • 修改个人资料
  • 保存敏感配置
  • 提交实名认证资料
  • 普通隐私数据提交

前端生成 nonce 的防重放逻辑如下:

flowchart TD
    A[前端生成 nonce] --> B[提交敏感请求]
    B --> C[服务端读取 userId 和 nonce]
    C --> D{Redis 中是否存在}
    D -- 存在 --> E[判定为重复请求并拒绝]
    D -- 不存在 --> F[写入 Redis,TTL 约 5 分钟]
    F --> G[继续解密和处理业务]

后端生成 nonce

高风险操作可以改成后端下发一次性 nonce 或 challenge:

1
2
3
4
5
1. 前端请求后端获取 nonce
2. 后端生成 nonce,绑定用户、接口、业务场景并写入 Redis
3. 前端提交敏感操作时带回 nonce
4. 后端验证 nonce 存在、未过期、未使用
5. 验证通过后消费 nonce

优点是安全性更强。后端不仅能判断 nonce 有没有重复,还能判断它是不是由自己签发、是否属于当前用户、是否只能用于当前接口。

适合:

  • 支付
  • 提现
  • 转账
  • 修改密码
  • 换绑手机号
  • 绑定银行卡
  • 签署合同

后端生成 nonce 更像一次性业务凭证:

sequenceDiagram
    participant C as 前端
    participant S as 服务端
    participant R as Redis

    C->>S: 请求操作凭证
    S->>R: 保存 nonce、userId、action、expireAt
    S-->>C: 返回 nonce 或 challenge
    C->>S: 提交敏感操作和 nonce
    S->>R: 校验 nonce 是否存在且未使用
    R-->>S: 返回绑定信息
    S->>S: 校验用户、接口、业务上下文
    S->>R: 消费 nonce
    S-->>C: 返回处理结果

密钥管理

这类方案里,密钥管理比算法本身更容易出问题。

前端可以持有:

1
2
服务端 RSA 公钥
本次请求临时 AES 密钥

服务端必须保护:

1
2
3
RSA 私钥
nonce 缓存
用户登录态密钥

注意事项:

  • RSA 私钥不能放在前端。
  • AES 密钥应每次请求临时生成,不要长期复用。
  • AES-GCM 的 IV 在同一个 AES 密钥下不能重复。
  • 服务端私钥应定期轮换,并支持多版本 keyId。
  • 不要把解密后的敏感明文写入普通日志。
  • 错误信息不要返回过多细节,例如不要告诉攻击者是签名错、密钥错还是 padding 错。

如果系统要支持密钥轮换,可以在请求里增加:

1
2
3
{
"keyId": "rsa-key-2026-06"
}

服务端根据 keyId 选择对应私钥解密。

密钥边界可以用下面的图来理解:

flowchart LR
    subgraph Browser[前端浏览器]
        PUB[服务端 RSA 公钥]
        TEMP[本次请求临时 AES key]
        IV2[本次请求 IV]
    end

    subgraph Backend[服务端]
        PRI[服务端 RSA 私钥]
        NONCE[(nonce 缓存)]
        TOKEN[Token 验证密钥或会话]
    end

    subgraph Database[持久化系统]
        DB[(业务数据库)]
        LOG[(日志系统)]
    end

    PUB -->|可公开分发| Browser
    TEMP -->|仅随请求临时存在| Browser
    PRI -->|绝不下发前端| Backend
    Backend --> DB
    Backend --> LOG

什么时候不需要业务层加密

并不是所有接口都应该做业务层加密。

如果接口只是普通查询、非敏感配置、公开列表、普通文章内容,那么:

1
HTTPS + 登录认证

就已经足够。

业务层加密会带来额外成本:

  • 前后端复杂度增加
  • 排查问题更困难
  • 网关和日志无法直接观察业务字段
  • 加密失败会带来新的错误场景
  • 密钥轮换和兼容性需要额外设计

所以它更适合“少数敏感接口”,而不是全站所有接口。

常见错误做法

下面这些做法应尽量避免:

1
2
3
4
5
6
7
8
9
10
使用 AES-ECB
固定 IV
长期复用同一个 AES 密钥
前端硬编码私钥
用 RSA 直接加密整个请求体
用 MD5 做签名
只加密不做防重放
把 nonce 存在本地但后端不校验
解密后的敏感数据打印到日志
自定义一套没有审计过的加密算法

安全方案最怕“看起来加密了”。真正可靠的方案,不只是密文看起来复杂,而是密钥、随机数、签名、防重放、日志、错误处理都要闭环。

总结

Web 敏感接口的业务层加密可以采用一套清晰的分工方案:

1
2
3
4
5
6
7
HTTPS:保护传输通道
Token:识别用户身份
AES-256-GCM:加密业务数据
RSA-OAEP:加密本次请求的 AES 密钥
timestamp:限制请求有效期
nonce:防止请求重放
HMAC-SHA256:可选,用于统一签名防篡改

一句话概括:

1
AES 负责加密数据,RSA 负责安全传递 AES 密钥,timestamp + nonce 负责防重放,HMAC 负责补充签名校验。

最终方案可以收敛成下面这条链路:

flowchart LR
    A[业务 JSON] --> B[AES-256-GCM]
    B --> C[密文 data]
    D[AES key] --> E[RSA-OAEP]
    E --> F[encryptedKey]
    C --> G[HTTPS 请求]
    F --> G
    H[timestamp + nonce] --> G
    I[HMAC-SHA256 可选] --> G
    G --> J[服务端校验]
    J --> K[解密并执行业务]

对于普通接口,不要过度设计,HTTPS 就是第一道也是最重要的安全边界。对于实名认证、支付、提现、换绑手机号等高风险接口,再使用业务层加密,才能在安全性和系统复杂度之间取得比较好的平衡。