RESTful API 设计检查清单
当设计、测试或发布新的 RESTful API 时,您是在现有复杂系统之上构建新系统。至少,您是在 HTTP 之上构建,而 HTTP 又建立在 TCP/IP 之上,而 TCP/IP 又建立在一系列管道之上。您还在 Web 服务器、应用程序框架甚至 API 框架之上构建。
我不希望您在构建 API 时,只在遇到麻烦时才知道未知的未知数。因此,这里列出了在设计、测试、实施和发布 Web API 时很容易被忽略的一些显而易见和微妙的事情。
本文 RESTful API 是系列的文章之一:
RESTful API设计概要
RESTful API 状态码设计指南
RESTful API 设计检查清单
再次强调一些基本的RESTful API设计原则:
1、围绕资源组织API设计
2、根据HTTP方法定义API操作
3、媒体类型符合HTTP语义Content-Type
4、支持大型二进制资源的部分响应,资源可能包含大型二进制字段,如文件或图像。为了克服由不可靠和间歇性连接引起的问题,并缩短响应时间,请考虑以块方式检索此类资源。
5、版本化RESTful API
6、使用过滤器,防止返回所有数据
7、同步设计API的安全性
8、为用户提供API文档,可借助API工具实现自动化的 API文档生成。
9、为用户提供SDK,可借助API工具实现自动的 API客户端SDK生成。
HTTP
HTTP 1.1 规范RFC2616是一份长达 54,121 个字的厚重文档。以下是该规范中可能影响您的 API 设计的一些选定项目:
#1. 幂等方法– GET、HEAD、PUT、DELETE、OPTIONS 和 TRACE 均旨在成为幂等操作;也就是说,“N > 0 个相同请求的副作用与单个请求相同。”(RFC2616 §9.1.2)
#2. 身份验证– 大多数 API 都需要一种方法来识别和验证访问 API 的用户。HTTP为此提供了授权标头 ( RFC2616 §14.8 )。RFC2617 指定了特定的身份验证方案,包括最常见的 HTTP 基本身份验证。
许多流行的 API 都使用 HTTP 基本身份验证,以 API 密钥作为用户名或密码。这是一种简单有效的身份验证机制。
为了准确实现 HTTP 身份验证,如果请求因缺乏身份验证而未被允许,您应提供 401 状态代码和 WWW-Authenticate 标头。许多客户端在后续请求中发送 Authorization 标头之前都需要此响应。
#3. 201 Created – 使用“201 Created”响应代码表示请求已成功处理并导致创建新资源。201 响应可以在 Location 标头中包含新资源 URI。(RFC2616 §10.2.2)
#4. 202 已接受– 使用“202 已接受”响应代码表示请求有效且将被处理,但尚未完成。通常,这用于服务器端后台可能存在处理队列的情况。(RFC2616 §10.2.3)
#5. 4xx 与 5xx 状态代码– 4xx 与 5xx 状态代码之间存在重要区别:4xx 代码用于指示客户端错误,而 5xx 代码指示服务器端错误。正确使用这些状态代码类可以帮助应用程序开发人员了解他们是否做错了什么(4xx)或某些东西坏了(5xx)。(RFC2616 §6.1.1)(编辑:阅读“404 Not Found”真的是客户端错误吗?了解有关区别的更多信息。)
#6. 410 Gone – “410 Gone” 响应代码是一个未充分利用的响应代码,它通知客户端该 URL 上曾经存在某个资源,但现在已不存在。这可以在您的 API 中用于指示已删除、已存档或已过期的项目。(RFC2616 §10.4.11)
#7. 期望:100-Continue – 如果 API 客户端即将发送具有大型实体主体的请求,例如 POST、PUT 或 PATCH,它们可以在 HTTP 标头中发送“期望:100-Continue”,并等待“100 Continue”响应后再发送实体主体。这允许 API 服务器在浪费带宽返回错误响应(例如 401 或 403)之前验证请求的大部分有效性。支持此功能并不常见,但它可以提高 API 响应能力并在某些情况下减少带宽。(RFC2616 §8.2.3)
#8. 保持连接– 为多个 API 请求维护与 API 服务器的连接可以大大提高性能。如果配置正确,几乎每个 Web 服务器都应该支持保持连接。
#9. HTTP 压缩– HTTP 压缩既可用于响应主体(Accept-Encoding:gzip),也可用于请求主体(Content-Encoding:gzip),以提高 HTTP API 的网络性能。
#10. HTTP 缓存– 在您的 API 响应上提供 Cache-Control 标头。如果它们不可缓存,“Cache-Control: no-cache”将确保代理和浏览器理解这一点。如果它们是可缓存的,则需要考虑多种因素,例如缓存是否可以由代理共享,或者资源的“新鲜度”有多长。(RFC2616 §14.9)
#11. 缓存验证– 如果您有可缓存的 API 命中,则应在响应中提供 Last-Modified 或 ETag 标头,然后支持条件请求的 If-Modified-Since 或 If-None-Match 请求标头。这将允许客户端检查其缓存副本是否仍然有效,并防止在不需要时下载完整的资源。如果实施得当,您可以使条件请求比普通请求更高效,还可以节省一些服务器端负载。(RFC2616 §13.3)
#12. 条件修改– ETag 标头还可用于启用对资源的条件修改。通过在 GET 上提供 ETag 标头,后续的 POST、PATCH 或 DELETE 请求可以提供 If-Match 标头来检查它们是否在以上次看到的相同状态更新或删除资源。(RFC2616 §14.24)
#13. 绝对重定向– 这是 HTTP/1.1 鲜为人知的要求,即重定向(例如 201、301、302、303、307 响应代码)应该在 Location 响应标头中包含绝对 URI。许多客户端确实支持 Location 中的相对 URI,但如果您希望 API 与许多客户端广泛兼容,则应在任何重定向中使用绝对 URI。(RFC2616 §14.30)
#14. 链接响应标头– 在 RESTful API 中,即使响应的内容类型没有提供链接的自然方式(例如,PDF 或图像表示),也经常需要提供指向其他资源的链接。RFC5988指定了一种在响应标头中提供链接的方法。
#15. 规范 URL – 对于具有多个 URL 的资源,RFC6596定义了提供规范 URL 链接的一致方法。
#16. 分块传输编码– 如果您有大量内容响应,传输编码:分块是将响应流式传输到客户端的好方法。它将减少服务器和中间服务器的内存使用要求(尤其是实施 HTTP 压缩时),并提供更快的首字节响应时间。
#17. 分块传输编码中的错误处理– 在实施分块传输编码之前,先弄清楚如何处理请求中发生的错误。一旦开始流式传输响应,就无法更改 HTTP 状态代码。通常,您会在内容类型中定义一种表示错误的方法。
#18. X-HTTP-Method-Override – 一些 HTTP 客户端只能支持 GET 和 POST;您可以通过 POST 隧道传输其他 HTTP 方法,并使用事实上的标准 X-HTTP-Method-Override 标头来记录“真正的”HTTP 方法。
#19。URL 长度– 如果您的 API 支持复杂或任意过滤选项作为 GET 参数,请记住,如果 URL 长度超过 2000 个字符,客户端和服务器都可能存在兼容性问题。
API Design
#20. 无状态– 有一个地方你不希望你的 API 存储状态,那就是你的应用服务器。始终保持应用服务器无状态,这样它们才能轻松无痛地扩展。
#21. 内容协商——如果您想要支持资源的多种表现形式,则可以使用内容协商(例如,Accept 标头),或针对不同表现形式使用不同的 URL(例如,…?format=json),或者您可以将两者结合起来,让您的内容协商资源重定向到特定格式。
#22. URI 模板– URI 模板是一种定义明确的机制,用于为您的客户提供 URL 组合功能,或为您的最终用户记录您的 URL 访问模式。
#23. 为意图而设计——不要仅仅通过 API 公开内部业务对象。设计 API 时应使其具有语义含义,并与用户的使用情况相匹配。Darrel Miller在 API Craft 上发表了一篇很棒的文章,对此的描述比我更好。(编辑:我在一篇题为“停止设计脆弱的 Web API”的文章中尽了最大努力。)
#24. 版本控制– 理论上,如果您预先设计了一个非常棒的 API,您可能永远不需要在 API 中创建不兼容性。对于我们这些实用主义者来说,请在 API URL 中添加版本控制(例如 /v1/ 路径),这样当 API 无法按预期工作时,您就有了安全网。(编辑:一个扩展的理由是我的后续文章:没人有时间做这件事:API 版本控制)
#25. 授权– 在设计 API 时,请记住并非所有用户都有权访问系统中的所有对象。如果您使用或构建具有某种声明性安全性的 API 框架,那就太好了,这样就可以轻松分配和修改对资源的读写访问的授权。
#26. 批量操作– 如果大多数客户端能够发出更少的请求来获取或修改更多数据,那么它们的性能会更好。将批量操作构建到 API 中以支持此类用例是一个好主意。
#27. 分页– 分页在 API 中有两个主要用途:减少传递给客户端的不必要数据量,并减少应用服务器上的不必要计算量。制作分页集合资源有许多不同的模式;如果您不知道从哪里开始,请浏览 Stackoverflow 获取一些提示。如果您想成为我的个人英雄,请通过提供带有时间戳或版本的其他页面的链接来实现一致的分页,这样即使涉及的对象发生变化,您也不会在分页请求中看到重复的结果。
#28. Unicode – 如今,很明显您需要在 Web 服务中支持多种字符,而不仅仅是英语字符;在设计和测试 API 时请记住这一点。特别是,如果您将 Unicode 字符用作 URL 中的自然键(例如,/users/jimbob/ 变为 /users/싸이/),它们可能会很有趣。
#29. 错误日志记录– 确保你设计了 API 执行错误日志记录的方式,而不是随便拼凑起来。特别是,我发现区分由客户端输入引起的错误和由你的软件引起的错误非常有价值。将它们保存在两个单独的日志中。
Content
#30. 内容类型– 整本书都可以写关于内容类型的内容;我要指出的是,内容类型非常重要。就我个人而言,我喜欢重复使用其他人开发的内容类型,例如Atom、Collection+JSON、JSON HAL或 XHTML。定义自己的内容类型比您想象的要费力得多。
#31. HATEOAS – 超媒体作为应用程序状态的引擎是一种 REST 约束,简单来说,就是您的内容应该通过链接和表单告诉客户端下一步可以做什么。如果您在构建 API 时考虑到这一约束,那么它将更能适应变化……如果您的客户端也遵循您的设计方法。
#32. 日期/时间– 当您在 API 中提供日期/时间值时,请使用包含时区信息的格式。RFC3339是ISO 8601 的一个子集,是最简单的日期和时间格式。
Security
#33. SSL – 考虑是否应该在 HTTP 和 HTTPS 下提供 API,还是只提供 HTTPS。只提供 HTTPS 是一种越来越流行的选择。
#34. 跨站点请求伪造 (CSRF) – 如果您的 API 接受与交互式用户相同的身份验证配置,那么您可能容易受到 CSRF 攻击。例如,如果您的交互式用户登录并获取“SESSIONID”cookie,并且该 cookie 也可用于调用 API 请求,那么精心编写的HTML 表单可能会代表您的用户发出意外的 API 请求。(编辑:阅读更多有关保护 API 免受 CSRF 攻击的信息)
#35. 限制– 确保某个 API 用户不会通过编写非常愚蠢的 API 客户端来破坏您的系统。这种情况会发生,无论是意外还是恶意。如果 API 用户超出了您应为其提供的宽松 API 请求限制,请向他们提供带有Retry-After 标头的503 响应。
#36. 微妙的拒绝服务– 限制应该可以以最简单的方式阻止某人破坏您的 API,但也存在许多微妙的拒绝服务攻击。Slowloris、Billion laughs和ReDoS是有趣的 DoS 攻击示例,这些攻击不需要大量源资源,但它们会使您的 API 很快耗尽资源。
Client
无论您向用户提供测试代码还是为他们构建 SDK,请检查您提供给他们的客户端是否遵循一些简单的规则:
#37. 连接保持活动– 一些 HTTP 客户端库要求您做一些额外的工作来启用持久连接。持久连接可能会对 API 的感知性能产生重大影响。
#38. 授权前 401 – HTTP 客户端的另一个怪癖是,它们通常需要“401 未授权”响应,然后才会发出带有授权标头的请求。这会让您的 API 请求耗费大量时间,尤其是在高延迟难以应对的移动网络上。
#39. 期望:100-继续– 我知道至少有一个 API 客户端默认使用“期望:100-继续”;如果它没有收到“100 继续”响应,它将在 3 秒超时后继续请求。如果您不支持“100 继续”,这将是另一个可以在客户端禁用的性能拖累。
Other Stuff
#40. 文档– 编写 API 文档可能非常无聊,但手写文档通常是最好的文档。务必包含一些可运行的代码或 curl 命令行,以帮助人们尽快掌握。您还可以查看文档工具,例如apiary.io、Mashery I/O Docs或Swagger。
#41. 与客户一起设计! – 不要在真空中设计 API;与客户及其用例合作。这将帮助您“为意图而设计”(#23)。
#42. 反馈– 确保为 API 用户提供一种反馈 API 的方法。这可以通过您的支持渠道、托管论坛或邮件列表来实现。尽可能让您的用户获得无摩擦反馈。
#43. 自动化测试– 您的 API 应该是您构建自动化测试最简单的东西。毕竟,它是为自动化而设计的。充分利用它吧!