正文
API 接口上线前,不能只看“前端能调通”。接口一旦发布到公网或内网网关,别人可以绕过页面、改请求参数、换 token、并发重放、批量导出、构造错误输入。按钮隐藏了,不代表接口安全;页面不展示,不代表接口不能被调用。
真实事故已经反复说明这一点。
2023-01-19,T-Mobile 披露一起影响 3700 万客户的数据事件。AP 报道称攻击者从 2022-11-25 左右开始访问数据,T-Mobile 在 2023-01-05 发现问题;Wired 进一步提到攻击者通过 API 访问了客户姓名、邮箱、电话号码、账单地址、生日、账号和套餐信息。这个案例对开发者的提醒很直接:API 能被正常调用,不代表它应该允许被持续批量调用。
2021-05-05,TechCrunch 报道 Peloton 的 API 漏洞:即使用户把资料设为 private,攻击者也能通过 API 拉到私密账号数据。问题不在页面,而在服务端没有正确检查调用者是否有权拿这些字段。API 安全不能依赖“用户在网页上看不到入口”。
OWASP API Security Top 10 2023 把 Broken Object Level Authorization 放在第一项。2026 年一篇分析 100 多个 bug bounty 披露的 BOLA 研究也指出,在确认的 BOLA 案例中,未经授权的状态变更和直接对象引用是主要类型。换句话说,最常见的 API 风险不是复杂加密算法,而是“用户 A 能不能拿用户 B 的数据或替用户 B 操作”。
上线前先查这五件事:
| 检查项 | 要证明什么 |
|---|---|
| 鉴权 | 没有合法身份不能调用私有接口 |
| 越权 | 用户只能操作自己有权操作的资源 |
| 限流 | 批量请求、短信、邮件、导出、搜索不会被刷爆 |
| 幂等 | 重试、双击、网络抖动不会重复扣款或重复创建 |
| 错误返回 | 客户端能处理,攻击者拿不到内部细节 |
先列出哪些接口真的要上线
很多问题来自“以为没上线”。测试接口、调试接口、旧版本接口、管理接口、导出接口,只要能被路由到,就算上线。
先做一张接口清单:
| 字段 | 示例 |
|---|---|
| 方法和路径 | POST /api/orders |
| 入口 | 公网、内网、管理后台、移动端 |
| 调用身份 | 匿名、登录用户、管理员、系统服务 |
| 操作资源 | 订单、用户、文件、账单、消息 |
| 是否写操作 | 查询、创建、更新、删除、导出 |
| 是否高成本 | 发短信、发邮件、调用模型、生成文件 |
| 上线标准 | 通过鉴权、越权、限流、幂等和错误测试 |
用命令先扫项目里的路由定义:
rg -n "router\\.|app\\.(get|post|put|patch|delete)|@Get|@Post|@Put|@Patch|@Delete|defineEventHandler|server/api" src server apps如果接口没有 owner、没有调用身份、没有错误码和没有验证方式,先不要发布。
鉴权先查有没有匿名入口
鉴权检查回答的是:这个接口是否需要知道调用者是谁。
先用无 token 请求:
curl.exe -i https://api.example.com/api/orders再用无效 token 请求:
curl.exe -i https://api.example.com/api/orders -H "Authorization: Bearer invalid-token"通过标准:
| 场景 | 期望 |
|---|---|
| 没有 token | 私有接口返回 401 |
| token 无效 | 返回 401,不要降级成匿名用户 |
| token 过期 | 返回 401 或明确的过期错误 |
| 权限不足 | 返回 403,不要返回资源内容 |
| 公共接口 | 只返回公开字段,不返回内部字段 |
不要只在前端路由里判断登录。后端每个私有接口都要校验身份。网关校验也不够,业务服务仍要知道当前用户是谁、角色是什么、租户是什么。
越权用两个账号测
越权检查回答的是:用户能不能访问不属于自己的资源。OWASP 对 BOLA 的说明很明确:只要接口从客户端接收对象 ID,并基于这个 ID 访问数据,就要做对象级授权检查。
准备两个普通账号:
用户 A:创建订单 order_a
用户 B:创建订单 order_b用用户 B 的 token 访问用户 A 的资源:
curl.exe -i https://api.example.com/api/orders/order_a `
-H "Authorization: Bearer user-b-token"再试更新和删除:
curl.exe -i -X PATCH https://api.example.com/api/orders/order_a `
-H "Authorization: Bearer user-b-token" `
-H "Content-Type: application/json" `
-d "{\"status\":\"cancelled\"}"通过标准:
| 请求 | 期望 |
|---|---|
| B 读 A 的订单 | 403 或 404 |
| B 改 A 的订单 | 403 或 404 |
| B 删 A 的订单 | 403 或 404 |
B 传 userId=A 创建数据 | 后端忽略请求里的 userId,使用 token 身份 |
| 普通用户访问管理接口 | 403 |
不要写这种逻辑:
从 request.body.userId 读取用户 ID,然后查询订单。更稳的是:
从 token/session 得到 currentUserId。
查询订单时同时带上 resourceId 和 currentUserId / tenantId。
写操作前检查当前用户对资源的 action 权限。如果系统是多租户,还要加租户维度。id 对了但 tenantId 不对,也必须拒绝。
管理接口要查功能级权限
对象级授权解决“这条数据是不是你的”。功能级授权解决“你是不是能做这个动作”。
重点查这些接口:
| 接口 | 风险 |
|---|---|
/api/admin/* | 普通用户可能访问管理功能 |
/api/users/{id}/role | 普通用户提升自己或别人权限 |
/api/orders/export | 批量导出敏感数据 |
/api/refund | 重复退款、越权退款 |
/api/feature-flags | 打开未发布功能 |
/api/internal/* | 内部接口被公网路由暴露 |
测试方式:
1. 用普通用户 token 调管理员接口。
2. 用只读用户 token 调写接口。
3. 用同租户低权限用户调高权限接口。
4. 用跨租户用户调同名资源接口。
5. 用系统服务 token 调用户接口,确认 scope 不过宽。管理权限不要只靠菜单隐藏。只要接口存在,权限就必须在服务端执行。
限流要按资源成本设置
限流不是只防 DDoS。OWASP API4 提到,API 请求会消耗网络、CPU、内存、存储,也可能触发短信、邮件、电话、第三方 API 和云服务账单。没有限制时,攻击者不一定要打垮系统,也能把成本打上去。
先给接口分级:
| 接口类型 | 限制重点 |
|---|---|
| 登录 / 验证码 | 按账号、IP、设备限制,防爆破和短信轰炸 |
| 搜索 / 列表 | 限制分页大小、查询复杂度和频率 |
| 文件上传 | 限制大小、类型、总量、并发 |
| 导出 / 报表 | 限制频率、后台任务、审批或队列 |
| 发送邮件 / 短信 | 限制单用户、单手机号、单 IP、每日总量 |
| AI / 第三方 API | 设置预算、并发、超时和熔断 |
用脚本做最小压测:
1..30 | ForEach-Object {
curl.exe -s -o NUL -w "%{http_code}`n" https://api.example.com/api/sms/send `
-H "Authorization: Bearer user-a-token"
}通过标准:
| 情况 | 期望 |
|---|---|
| 连续请求超过阈值 | 返回 429 |
返回 429 | 带 Retry-After 或可理解的重试信息 |
| 导出接口频繁调用 | 进入队列或拒绝 |
| GraphQL 批量请求 | 限制 depth、cost、batch 数 |
| 第三方 API 异常 | 不无限重试,不拖垮自己 |
GitHub REST API 文档把 primary rate limit、secondary rate limit、认证/匿名请求的不同配额拆开说明,这是成熟 API 的基本做法:限流不是一个全局数字,而是按身份、资源、操作和成本分层。
幂等要覆盖创建、支付和重试
幂等检查回答的是:同一个业务动作被重复提交时,会不会重复生效。
最容易出问题的接口:
| 接口 | 常见事故 |
|---|---|
| 创建订单 | 双击提交生成两笔订单 |
| 支付回调 | 重复回调导致重复发货 |
| 退款接口 | 重复请求导致重复退款 |
| 发券 / 发积分 | 网络重试导致重复发放 |
| 创建用户 | 超时重试导致重复账号或脏数据 |
| 消息发送 | 用户收到多封邮件或多条短信 |
Stripe 的 API 文档建议对创建或更新对象的请求使用 idempotency key,服务端用这个 key 识别重试,避免同一操作重复执行。它还提醒不要用邮箱、个人标识这类敏感数据当幂等 key。
一个最小约定:
POST /api/orders
Idempotency-Key: 7f0e2d0e-3c9b-4a2c-8a61-7a0f1e0a1111服务端要保存:
| 字段 | 用途 |
|---|---|
idempotency_key | 识别同一次业务请求 |
user_id / tenant_id | 防止 key 被跨用户复用 |
request_hash | 防止同一个 key 带不同参数 |
status | processing / succeeded / failed |
response_body | 重复请求返回相同结果 |
expires_at | 控制保留时间 |
测试方式:
$key = "11111111-1111-4111-8111-111111111111"
curl.exe -i -X POST https://api.example.com/api/orders `
-H "Authorization: Bearer user-a-token" `
-H "Idempotency-Key: $key" `
-H "Content-Type: application/json" `
-d "{\"sku\":\"basic\",\"quantity\":1}"
curl.exe -i -X POST https://api.example.com/api/orders `
-H "Authorization: Bearer user-a-token" `
-H "Idempotency-Key: $key" `
-H "Content-Type: application/json" `
-d "{\"sku\":\"basic\",\"quantity\":1}"通过标准:只创建一次订单。第二次返回第一次的结果,或者返回明确的幂等冲突,不要再执行一次业务动作。
错误返回要能调试但不能泄露
错误返回有两个目标:客户端能处理,攻击者拿不到内部细节。
不要把这些内容返回给客户端:
- SQL 语句。
- stack trace。
- 内部文件路径。
- Redis key、S3 bucket、队列名。
- token、cookie、手机号、邮箱全量。
- 详细权限规则和内部角色名。
RFC 9457 定义了 HTTP API 的 Problem Details 格式,可以用来统一错误响应。你不一定要完全照搬,但至少要固定字段。
{
"type": "https://api.example.com/problems/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "You do not have permission to access this resource.",
"instance": "/request/req_123456"
}建议错误码:
| 场景 | 状态码 |
|---|---|
| 未登录或 token 无效 | 401 |
| 已登录但无权限 | 403 |
| 资源不存在或不想暴露存在性 | 404 |
| 参数格式错误 | 400 |
| 语义校验失败 | 422 |
| 限流 | 429 |
| 幂等冲突 | 409 |
| 服务端错误 | 500,但不要返回内部异常 |
日志里可以保存更多排查信息,但要脱敏。客户端拿 requestId,服务端用 requestId 查日志。
日志和审计要能追到谁做了什么
上线检查不能只看接口返回。出问题后,你要能回答:
- 谁调用了接口?
- 调用了哪个资源?
- 操作是否成功?
- 请求来自哪里?
- 是否命中限流?
- 是否重复请求?
- 是否触发权限拒绝?
建议每个关键接口记录:
request_id
user_id_hash
tenant_id
method
path_template
resource_type
resource_id_hash
action
status_code
auth_result
rate_limit_result
idempotency_key_hash
duration_ms不要记录完整 token、完整身份证、完整手机号、完整邮箱、原始密码、完整请求体。日志是排查工具,不是第二份用户数据库。
上线前跑一张最小检查表
把这张表放进 PR 或发布流程里。
API release check:
- [ ] All private endpoints reject anonymous requests
- [ ] Invalid and expired tokens return 401
- [ ] Normal user cannot access admin endpoints
- [ ] User B cannot read/update/delete User A resources
- [ ] Tenant boundary is checked on every tenant resource
- [ ] Request body userId / tenantId is not trusted directly
- [ ] List endpoints have pagination and maximum page size
- [ ] Costly operations have per-user and global limits
- [ ] Retry-sensitive POST endpoints support idempotency
- [ ] Duplicate payment/order/refund/webhook requests do not repeat side effects
- [ ] Errors return stable codes and requestId
- [ ] Errors do not expose SQL, stack trace, token or internal path
- [ ] Logs can trace requestId without storing secrets
- [ ] Old, debug and internal endpoints are not publicly routable再用两组账号做验收:
账号 A:普通用户,有自己的资源
账号 B:普通用户,有自己的资源
管理员 C:只用于管理接口测试
必须验证:
1. A 不能操作 B 的资源。
2. B 不能操作 A 的资源。
3. A / B 不能访问管理员接口。
4. 管理员接口也要记录审计日志。
5. 重复提交创建、支付、退款,不会重复生效。API 上线不是把 Controller、Route 或 Handler 合进去就结束。真正的上线标准是:身份说得清,资源边界守得住,高成本操作刷不爆,重复请求不会造成二次副作用,错误返回能排查但不泄密。
只要这五件事没有跑过,接口就还只是“能用”,不是“能上线”。