# 接口契约：参数、身份、权限与错误

原文: https://yishto.com/learn/service-data/http-api-contracts-and-access/

作者: [Yishto](https://yishto.com/about/)

以私有草稿编辑契约连接 JSON 验证、身份与对象权限，处理安全错误、超时重试、分页和并发版本。

首次发布: 2026-11-10
内容更新: 2026-11-10

阅读清单接口接收资料 ID 和新文本。若后端只检查用户已经登录，就可能让任何登录者改别人的记录；若只检查字段存在，还可能把数组当成文本保存。接口契约要同时表达数据、身份、资源权限和失败方式。

案例是一条合成草稿的编辑接口，只有所有者可修改。代码只在内存中返回状态数字，没有真实 HTTP 服务器、令牌验证或数据库。用它观察授权顺序与数据变化，再说明真实服务需要补哪些边界。

## 契约先定义调用与结果

接口说明应包含方法、路径、参数位置、请求结构、响应结构、身份要求、权限、错误码和重试语义。编辑草稿可以使用一个明确的资源路径，文本字段规定类型与长度，成功返回最终保存的版本，而不是只说“调用接口更新”。

JSON 是序列化格式，不自带业务规则。解析成功后还要检查对象类型、必需字段、未知字段策略和范围。限制请求体大小，避免先把无限输入读进内存再检查长度。字符串长度、字节长度和用户看到的字符数量也不是完全相同的概念，契约需要说清。

OpenAPI 可以描述操作、schema 和安全方案，帮助生成文档与检查，但不会自动实施每条权限规则。文档中写了“仅本人”，服务端仍要对每次资源操作验证。真实实现与文档同步需要测试与审阅。

## 身份认证与资源授权分开执行

认证确定请求身份，例如由服务端验证会话或访问令牌。客户端传入的 owner 字段不能证明归属；后端应从可信身份上下文获取用户，再查询或限定其可操作资源。

授权回答该身份能否对这条资源执行这个动作。读取、编辑、删除、导出可能有不同规则。仅要求“已登录”只解决身份层，没有限制记录范围，这正是对象级授权缺失的一种常见形式。

列表和单条接口都要过滤。列表只能看到自己的记录，不代表通过猜 ID 的详情接口也安全；更新时应把资源范围纳入实际读取或写入条件。并发环境中，检查与修改之间的数据变化也要由事务或条件更新处理。

## 错误状态要有稳定含义

401 表示缺少或无效的认证，真实 HTTP 响应应按认证方案提供相应挑战信息；403 表示服务器拒绝已理解的操作。对于不允许泄露存在性的私有记录，可以统一返回 404，不让其他用户区分“不存在”和“不属于你”。

参数错误需要稳定应用错误码与安全说明，不把数据库堆栈返回给调用者。使用 400、422 等状态时按项目契约保持一致，别让客户端解析自然语言猜失败类型。日志可以包含安全的关联 ID，响应则提供读者能执行的修正建议。

错误隐藏并非权限替代。无论返回 403 还是 404，都必须确保未授权操作没有副作用。也不要因为后台日志便于排查，就记录完整令牌、正文和个人数据。诊断与公开错误需要不同信息范围。

## 离线验证所有权与修改结果

记录 D-1 属于 U-1。函数先检查是否有身份，再判断记录是否存在且属于该用户，然后检查文本。U-2 的修改返回 404，并且原文本必须保持 draft；U-1 可以把文本改为 ready。

```python
records = {"D-1": {"owner": "U-1", "text": "draft"}}
def edit(user, record_id, text):
    if user is None:
        return 401
    record = records.get(record_id)
    if record is None or record["owner"] != user:
        return 404
    if not isinstance(text, str) or not 1 <= len(text) <= 100:
        return 400
    record["text"] = text
    return 200
assert edit("U-2", "D-1", "changed") == 404
assert records["D-1"]["text"] == "draft"
assert edit("U-1", "D-1", "ready") == 200
print(records["D-1"]["text"])
```

输出为 `ready`。代码展示了拒绝跨用户修改与成功写入，长度规则按 Python 字符串元素数量计算。它不模拟 HTTP 头、认证挑战、Unicode 字节预算或并发事务，因此不能称为完整安全 API。

示例把缺失资源和非所有者统一处理，但把参数检查放在授权之后。真实服务可先做最小格式与体积检查，避免无效请求消耗过多资源，再做身份与对象规则。检查顺序应兼顾信息暴露、资源消耗与稳定错误契约。

## 重试需要判断业务效果

HTTP safe 表示方法设计上不要求改变资源，idempotent 表示多次相同请求的预期效果与一次相同。日志等附带工作仍可能发生。这些语义不能自动证明一次付款、任务创建或草稿版本递增只发生一次。

调用超时可能发生在服务器已提交但客户端没收到响应时。对会产生重复效果的操作，定义请求幂等键、保存结果与重复返回规则，或提供查询最终状态的方法。客户端只因超时就无限重试，会扩大重复操作与负载。

限流、服务不可用与网络失败可以有不同重试条件。规定最大次数、总时间预算和退避；若服务提供 Retry-After，按契约处理。权限与参数错误通常需要修改条件，不适合原样不断重发。

## 分页与版本也是契约的一部分

列表需要稳定排序和分页边界，明确游标是否绑定筛选条件。数据在翻页间变化时，不要暗示必然无重复无遗漏。根据产品需求选择快照、游标或去重策略，并说明条件。

编辑需要考虑两位客户端同时保存。可以让客户端提交已读版本，服务端只在版本匹配时更新，否则返回冲突。若接受最后写入覆盖，也应明确这项策略，而不是把数据丢失留给用户发现。

未知字段的策略也要有意选择。严格拒绝能尽早发现调用方拼写错误，忽略未知字段则可能支持渐进扩展；两种方式都应写进契约。对于批量编辑，明确是整体失败还是逐项结果，不能在部分记录已写入后只返回一个模糊错误，让客户端不知道哪些动作需要恢复。

## 练习：让错误路径没有副作用

为草稿接口写出请求结构、身份来源、所有权规则、错误码和重试条件。测试未登录、所有者、另一用户、不存在 ID、空文本、过长文本和错误字段类型。对每个失败保存修改前后的记录比较。

验收要求：只有所有者合法输入能改变草稿；隐藏存在性的策略一致；错误码可由客户端稳定判断；超时后能查询或安全重试；两个旧版本编辑不会悄悄覆盖。离线断言仅验证简化规则，真实接口还需独立验证认证、HTTP 响应与数据库提交。

## 资料来源与延伸阅读

- [HTTP overview](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview)
- [OpenAPI specification](https://spec.openapis.org/oas/latest.html)
- [API security risks](https://owasp.org/API-Security/editions/2023/en/0x00-header/)
- [JSON format — RFC 8259](https://www.rfc-editor.org/rfc/rfc8259.html)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [MCP 与工具调用：输入契约、权限与执行边界](https://yishto.com/learn/agents/tool-contracts-and-mcp/)
- [从网址到页面：一次请求经过哪些边界](https://yishto.com/learn/computing/web-request-journey/)
