ITPub博客

首页 > 数字化转型 > 其他 > API 安全设计:五道防线,以及各自的边界

API 安全设计:五道防线,以及各自的边界

原创 其他 作者:LindaLin 时间:2026-10-14 16:55:11 0 删除 编辑

"接口安全"这个词经常被当成一件事,但它其实是 五件不同的事——身份认证、权限控制、防重放、输入校验、频率配额。

每一道防线只解决一类问题。 做了其中三道就以为安全了,是这类系统最常见的问题来源。

这篇把五道防线拆开讲清楚:各自防什么、怎么实现、以及 明确它挡不住什么。最后一条尤其重要—— 知道边界,才知道哪里还需要补。


一、五道防线:先分清各自防什么

防线

防的是什么

核心机制

挡不住什么

① 身份认证

他是谁

密钥 / 令牌 / 签名

凭证泄露后的滥用

② 权限控制

他能干什么

按接口 / 资源授权

横向越权(需要归属校验)

③ 防重放

旧请求被重发

时间戳 + 随机数

凭证泄露后的实时滥用

④ 输入校验

参数有问题

类型 / 范围 / 白名单

逻辑层的越权

⑤ 频率与配额

他调得太多

按调用方限流

合法的慢速滥用

最后那一列是这张表最重要的部分。 很多团队在做过③④之后就觉得"安全做完了",但 "凭证泄露"和"越权"这两个最严重的场景,恰恰是前面几道防线都挡不住的——因为它们都是"合法凭证发起的非法请求"。


二、第一道:身份认证 —— 凭证必须可轮换

三种常见形态

形态一:长期密钥 + 请求签名。 每次请求用密钥算签名,服务端验算。

  • 优点: 无状态,服务端不需要存储令牌;适合服务端之间的调用;
  • 缺点: 密钥一旦泄露,影响面最大,且有效期长期不变。

形态二:短期令牌。 先用长期凭证换取短期令牌,再用令牌调用。

  • 优点:有效期短, 即使泄露,损失也被限制在有效期内;可以主动吊销;
  • 缺点:需要处理令牌刷新,以及 并发刷新的问题(这个坑在网关层解决最合适)。

形态三:授权码流程。 用户授权后换取访问凭证,代表用户操作。

  • 优点: 权限归属于用户,用户可随时撤销;
  • 缺点:流程最长,接入成本最高。

选型建议: 服务端之间用形态二(短期令牌);代表用户操作、需要用户可控时用形态三;形态一仅在无法改造的老系统上保留,并务必加上 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)

两个关键点:

  • 用原子的"设置并判断"(SET NX),不能"先查再写"——否则并发下有窗口期;
  • 过期时间必须 ≥ 时间戳窗口,否则随机数先过期,就失去了去重能力。

边界提醒:防重放挡不住"凭证泄露后的实时滥用"。 如果密钥被第三方拿到,他完全可以构造全新的、时间戳最新的合法请求——防重放对此无能为力。 这类问题的解法只有"缩短凭证有效期"和"异常行为检测"。


五、第四道:输入校验 —— 不只是防注入

输入校验常被理解为"防注入",但它的作用远不止于此。

三个层次:

① 类型与范围。 数字就是数字、长度有上限、枚举值在白名单内。这一层防的是 "非法输入导致的异常"。

② 查询参数白名单。 这一点在列表接口上尤其重要: 排序字段、筛选字段必须走白名单。

为什么这和安全有关?因为 允许用户按任意字段排序或筛选,等于把数据库的结构暴露出去——通过观察响应时间可以推断出哪些字段有索引、哪些表有多大。而且这类请求往往极耗资源。

③ 输出侧也要校验。 这一层经常被忽略: 返回给调用方的字段必须是显式列出的,不能"把数据库行直接序列化返回"。

原因很直接: 如果返回全字段,那么新增一个敏感字段(比如内部备注、标记载荷、成本价)时,它会自动被暴露出去。 而你可能根本不会意识到。

边界提醒:输入校验挡不住"逻辑层的越权"。 参数格式完全合法,但请求的是别人的数据——这需要权限与归属校验来解决,输入校验帮不上忙。


六、第五道:频率与配额 —— 按调用方,不只按接口

限流的技术实现(算法、分布式)在《接口限流设计》里讲过,这里补充 安全视角的两个要点:

① 必须按调用方维度限流,不能只按接口维度。

只按接口限流的话, 所有调用方共享一个总量——一个人狂调,全平台遭殃。而且你无法识别是谁在滥用。

② 配额和用量要能被看见。

  • 调用方能查到自己用了多少、还剩多少—— 这本身就是一种自我保护机制:正常用户发现配额快满了会主动降速;
  • 你要能发现异常: 某个调用方的用量突然上涨、或在非工作时段高频调用,这是需要关注的信号。

边界提醒:配额挡不住"合法的慢速滥用"。 把频率控制在限额之下,长期缓慢地拉取数据—— 这类行为在指标上完全正常。 识别它需要看 "调用内容的模式"(比如是否在顺序遍历 ID、是否反复查询同一批资源),而不是看频率。


七、越权:最容易被忽略的一类风险

越权是实践中最高发的 API 安全问题,而且 前面四道防线都可能挡不住它。分两类:

横向越权:看到了别人的数据

典型表现: 请求里带上资源 ID,服务端只校验了"你有没有登录",没校验"这个资源是不是你的"。

GET /orders/10086            ← 我的身份是 A
请求头带 A 的合法凭证
但订单 10086 属于 B

如果服务端只验证了凭证合法,就直接返回了订单内容,这就是横向越权。

对策:

  • 资源归属校验必须下沉到数据访问层,靠框架强制(比如查询条件自动附加归属过滤),而不是靠每个开发者记得写;
  • 归属校验要覆盖所有入口:详情、列表、导出、批量操作、统计报表—— 漏掉任何一个都会出问题;
  • 跨权限返回"不存在"而不是"无权限":返回 403 本身就在泄露"这个资源存在"。

纵向越权:做了超出权限的事

典型表现: 普通调用方访问到了本该只有管理员能用的接口。

这里有一个常见误区: 把"接口没在文档里暴露"当成安全措施。 这叫"隐藏式安全",实际没有保护作用——路径可以被猜、前端代码可以被读、路由可以被枚举。

对策:

  • 按接口粒度授权,默认拒绝;
  • 权限变更要有审计日志,能回答"谁在什么时候给谁开了什么权限"。

八、几个高频的实践问题

① 密钥怎么存?

不要在代码里、不要在明文配置里。用配置中心或密钥管理服务,并且 应用只持有它需要的那部分密钥。

② 日志能记什么?

日志是泄露的重灾区。 密钥、令牌、用户敏感字段绝对不进日志。这在网关层尤其重要——它是全公司流量的必经之地。

③ 错误信息能给多详细?

给外部看的信息要少,给自己看的信息要多。 对外统一返回"请求失败"加一个可追踪的标识;详细堆栈、SQL、内部字段名只写进内部日志。

④ 内部接口要不要做安全?

要,但可以简化。 边界是"网络位置"而不是"心理假设"——不要假设"内网就安全",很多问题都是从内网横向扩散开始的。

⑤ 怎么验证做得对不对?

必须写自动化测试。 特别是越权场景:构造 A 的身份去请求 B 的资源,断言返回失败。 这类测试应该覆盖每个资源型接口——靠人工回归一定会漏。


九、反模式表

反模式

后果

正确做法

密钥硬编码或进仓库

泄露后无法挽回

配置中心 / 密钥管理

凭证不支持轮换

出了事不敢换

双密钥并存 + 使用情况可查

只做认证不做授权

认证即全权

接口级 + 资源级授权

只做接口级授权

能查订单变成能查所有订单

资源归属校验

签名不含时间戳

请求可无限重放

时间戳纳入签名范围

随机数用「先查再写」

并发有窗口期

原子 SET NX

随机数过期短于时间戳窗口

失去去重能力

过期时间 ≥ 窗口

允许按任意字段排序筛选

暴露库结构、拖垮数据库

白名单

直接序列化数据库行返回

新增敏感字段自动泄露

显式声明返回字段

只按接口维度限流

一人狂调全平台遭殃

按调用方维度

把"接口未公开"当安全

无实际保护作用

显式授权,默认拒绝

越权返回 403

泄露资源存在

统一返回 404

把密钥打进日志

泄露面最大

脱敏,敏感字段不打

对外返回详细错误

泄露内部结构

对外简略 + 内部详细

假设内网安全

横向移动风险

内网同样鉴权

越权靠人工测试

一定漏

自动化测试,覆盖每个资源接口


十、上线前检查清单

认证与凭证

  • 凭证支持轮换(双密钥并存、平滑切换)
  • 密钥不在代码仓库、不在明文配置
  • 能查到每个调用方在用哪个密钥
  • 有明确的轮换周期,不是「等出事再换」
  • 敏感操作的凭证有效期更短

权限

  • 接口级授权,默认拒绝
  • 资源级归属校验,且下沉到数据访问层
  • 归属校验覆盖所有入口(详情 / 列表 / 导出 / 批量 / 统计)
  • 越权统一返回「不存在」而非「无权限」
  • 权限变更留有审计日志

防重放

  • 签名范围包含时间戳与随机数
  • 服务端校验时间戳在允许窗口内
  • 随机数用原子「设置并判断」,有去重与过期
  • 随机数过期时间 ≥ 时间戳窗口

输入与输出

  • 参数有类型、范围、长度校验
  • 排序与筛选字段走白名单
  • 分页参数有上限
  • 返回字段显式声明,不直接序列化数据行
  • 新增字段不会自动暴露给调用方

频率与配额

  • 限流按调用方 + 接口双维度
  • 调用方能查自己的用量与剩余配额
  • 有异常用量告警(突增、非工作时段)

可观测与审计

  • 事件日志不包含密钥与敏感字段
  • 对外错误信息简略,内部日志详细
  • 关键操作(改权限、改配额、导出)有审计记录
  • 越权场景有自动化测试,覆盖每个资源型接口

一句话总结: API 安全的关键不在"加了多少道检查",而在 知道每道检查挡不住什么。认证挡不住越权,防重放挡不住凭证泄露,输入校验挡不住逻辑越权—— 真正的安全是让这些防线互补,而不是重复。 两条最该记住的铁律: 凭证必须可轮换,归属必须校验。

本文为通用安全设计方法总结,具体方案需结合业务敏感程度与合规要求确定,并建议在关键场景做专门的安全评估。


相关参考: 跨境电商平台接口与数据服务 方案


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70051425/viewspace-3185597/ ,如需转载,请注明出处,否则将追究法律责任。

下一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2026-09-21

  • 博文量
    2
  • 访问量
    1294