首页 > 数字化转型 > 其他 > API 安全设计:五道防线,以及各自的边界
"接口安全"这个词经常被当成一件事,但它其实是 五件不同的事——身份认证、权限控制、防重放、输入校验、频率配额。
每一道防线只解决一类问题。 做了其中三道就以为安全了,是这类系统最常见的问题来源。
这篇把五道防线拆开讲清楚:各自防什么、怎么实现、以及 明确它挡不住什么。最后一条尤其重要—— 知道边界,才知道哪里还需要补。

|
防线 |
防的是什么 |
核心机制 |
挡不住什么 |
|
① 身份认证 |
他是谁 |
密钥 / 令牌 / 签名 |
凭证泄露后的滥用 |
|
② 权限控制 |
他能干什么 |
按接口 / 资源授权 |
横向越权(需要归属校验) |
|
③ 防重放 |
旧请求被重发 |
时间戳 + 随机数 |
凭证泄露后的实时滥用 |
|
④ 输入校验 |
参数有问题 |
类型 / 范围 / 白名单 |
逻辑层的越权 |
|
⑤ 频率与配额 |
他调得太多 |
按调用方限流 |
合法的慢速滥用 |
最后那一列是这张表最重要的部分。 很多团队在做过③④之后就觉得"安全做完了",但 "凭证泄露"和"越权"这两个最严重的场景,恰恰是前面几道防线都挡不住的——因为它们都是"合法凭证发起的非法请求"。
形态一:长期密钥 + 请求签名。 每次请求用密钥算签名,服务端验算。
形态二:短期令牌。 先用长期凭证换取短期令牌,再用令牌调用。
形态三:授权码流程。 用户授权后换取访问凭证,代表用户操作。
选型建议: 服务端之间用形态二(短期令牌);代表用户操作、需要用户可控时用形态三;形态一仅在无法改造的老系统上保留,并务必加上 IP 或网络层限制。
这是第一条铁律: 任何不能更换的凭证,都是一颗定时炸 弹。
轮换这件事必须在设计之初就想清楚,因为 它决定了你的凭证存储和分发方式:
现实中大量系统"知道该轮换但没人敢换",根源都是 "不知道谁在用"。所以这件事的关键不在轮换动作本身,而在 先把使用情况摸清。
认证解决"你是谁",但 认证通过不等于什么都能做。
权限控制要做到两个粒度:
① 接口级授权。 明确"哪个调用方可以调哪些接口",而不是"认证了就能调所有接口"。
② 资源级授权。 明确"这个调用方可以操作哪些资源"。
一个常见错误是: 只做接口级授权,不做资源级。 结果是"能调订单查询接口"变成了"能查所有订单"——这就是越权。
默认拒绝的原则: 权限应该是显式授予的,未明确授权的接口一律拒绝。而不是"默认允许,特殊接口再限制"。
重放问题的原理很简单: 第三方截获一个合法请求,原样重发一遍。 因为签名是合法的,服务端无法区分这是新请求还是重放。
两种基本手段: 时间戳 + 随机数(nonce)。
如果签名只覆盖业务参数,那么 同一个签名可以无限次使用——第三方拿到一个合法的签名,就可以反复重放。
正确做法是把 时间戳纳入签名的计算范围:
def build_sign(secret: str, params: dict, timestamp: int, nonce: str) -> str:
# 时间戳与随机数必须参与签名,否则可被无条件重放
payload = {
**params,
"_ts": timestamp,
"_nonce": nonce,
}
canonical = "&".join(f"{k}={payload[k]}" for k in sorted(payload))
return hmac_sha256(secret, canonical)
服务端验签时要检查两件事: 签名正确,且 时间戳在允许的偏差窗口内(比如 ±5 分钟)。
时间戳窗口只能限制"多久之前的请求不能重放",但 在 5 分钟窗口内,同一请求仍可重复使用。
所以随机数是必需的:
def check_nonce(nonce: str, ttl: int = 600) -> bool:
"""返回 True 表示第一次出现(合法),False 表示重复"""
# SET NX 语义:只在不存在时设置成功
return redis.set(f"nonce:{nonce}", "1", nx=True, ex=ttl)
两个关键点:
边界提醒:防重放挡不住"凭证泄露后的实时滥用"。 如果密钥被第三方拿到,他完全可以构造全新的、时间戳最新的合法请求——防重放对此无能为力。 这类问题的解法只有"缩短凭证有效期"和"异常行为检测"。
输入校验常被理解为"防注入",但它的作用远不止于此。
三个层次:
① 类型与范围。 数字就是数字、长度有上限、枚举值在白名单内。这一层防的是 "非法输入导致的异常"。
② 查询参数白名单。 这一点在列表接口上尤其重要: 排序字段、筛选字段必须走白名单。
为什么这和安全有关?因为 允许用户按任意字段排序或筛选,等于把数据库的结构暴露出去——通过观察响应时间可以推断出哪些字段有索引、哪些表有多大。而且这类请求往往极耗资源。
③ 输出侧也要校验。 这一层经常被忽略: 返回给调用方的字段必须是显式列出的,不能"把数据库行直接序列化返回"。
原因很直接: 如果返回全字段,那么新增一个敏感字段(比如内部备注、标记载荷、成本价)时,它会自动被暴露出去。 而你可能根本不会意识到。
边界提醒:输入校验挡不住"逻辑层的越权"。 参数格式完全合法,但请求的是别人的数据——这需要权限与归属校验来解决,输入校验帮不上忙。
限流的技术实现(算法、分布式)在《接口限流设计》里讲过,这里补充 安全视角的两个要点:
① 必须按调用方维度限流,不能只按接口维度。
只按接口限流的话, 所有调用方共享一个总量——一个人狂调,全平台遭殃。而且你无法识别是谁在滥用。
② 配额和用量要能被看见。
边界提醒:配额挡不住"合法的慢速滥用"。 把频率控制在限额之下,长期缓慢地拉取数据—— 这类行为在指标上完全正常。 识别它需要看 "调用内容的模式"(比如是否在顺序遍历 ID、是否反复查询同一批资源),而不是看频率。
越权是实践中最高发的 API 安全问题,而且 前面四道防线都可能挡不住它。分两类:
典型表现: 请求里带上资源 ID,服务端只校验了"你有没有登录",没校验"这个资源是不是你的"。
GET /orders/10086 ← 我的身份是 A 请求头带 A 的合法凭证 但订单 10086 属于 B
如果服务端只验证了凭证合法,就直接返回了订单内容,这就是横向越权。
对策:
典型表现: 普通调用方访问到了本该只有管理员能用的接口。
这里有一个常见误区: 把"接口没在文档里暴露"当成安全措施。 这叫"隐藏式安全",实际没有保护作用——路径可以被猜、前端代码可以被读、路由可以被枚举。
对策:
① 密钥怎么存?
不要在代码里、不要在明文配置里。用配置中心或密钥管理服务,并且 应用只持有它需要的那部分密钥。
② 日志能记什么?
日志是泄露的重灾区。 密钥、令牌、用户敏感字段绝对不进日志。这在网关层尤其重要——它是全公司流量的必经之地。
③ 错误信息能给多详细?
给外部看的信息要少,给自己看的信息要多。 对外统一返回"请求失败"加一个可追踪的标识;详细堆栈、SQL、内部字段名只写进内部日志。
④ 内部接口要不要做安全?
要,但可以简化。 边界是"网络位置"而不是"心理假设"——不要假设"内网就安全",很多问题都是从内网横向扩散开始的。
⑤ 怎么验证做得对不对?
必须写自动化测试。 特别是越权场景:构造 A 的身份去请求 B 的资源,断言返回失败。 这类测试应该覆盖每个资源型接口——靠人工回归一定会漏。
|
反模式 |
后果 |
正确做法 |
|
密钥硬编码或进仓库 |
泄露后无法挽回 |
配置中心 / 密钥管理 |
|
凭证不支持轮换 |
出了事不敢换 |
双密钥并存 + 使用情况可查 |
|
只做认证不做授权 |
认证即全权 |
接口级 + 资源级授权 |
|
只做接口级授权 |
能查订单变成能查所有订单 |
资源归属校验 |
|
签名不含时间戳 |
请求可无限重放 |
时间戳纳入签名范围 |
|
随机数用「先查再写」 |
并发有窗口期 |
原子 SET NX |
|
随机数过期短于时间戳窗口 |
失去去重能力 |
过期时间 ≥ 窗口 |
|
允许按任意字段排序筛选 |
暴露库结构、拖垮数据库 |
白名单 |
|
直接序列化数据库行返回 |
新增敏感字段自动泄露 |
显式声明返回字段 |
|
只按接口维度限流 |
一人狂调全平台遭殃 |
按调用方维度 |
|
把"接口未公开"当安全 |
无实际保护作用 |
显式授权,默认拒绝 |
|
越权返回 403 |
泄露资源存在 |
统一返回 404 |
|
把密钥打进日志 |
泄露面最大 |
脱敏,敏感字段不打 |
|
对外返回详细错误 |
泄露内部结构 |
对外简略 + 内部详细 |
|
假设内网安全 |
横向移动风险 |
内网同样鉴权 |
|
越权靠人工测试 |
一定漏 |
自动化测试,覆盖每个资源接口 |
认证与凭证
权限
防重放
输入与输出
频率与配额
可观测与审计
一句话总结: API 安全的关键不在"加了多少道检查",而在 知道每道检查挡不住什么。认证挡不住越权,防重放挡不住凭证泄露,输入校验挡不住逻辑越权—— 真正的安全是让这些防线互补,而不是重复。 两条最该记住的铁律: 凭证必须可轮换,归属必须校验。
本文为通用安全设计方法总结,具体方案需结合业务敏感程度与合规要求确定,并建议在关键场景做专门的安全评估。
相关参考: 跨境电商平台接口与数据服务 方案
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70051425/viewspace-3185597/ ,如需转载,请注明出处,否则将追究法律责任。