← 服务端与数据 · 第 1 篇

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

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

学完这一篇,你能做到

设计草稿接口契约,并证明未授权或无效输入不改变记录,重试与并发规则明确。

阅读清单接口接收资料 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。

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 响应与数据库提交。

资料来源与延伸阅读

资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。