在参考了GitHub API设计和大量博客文章后总结了一下RESTful API的设计,分享如下。想要更好的理解RESTful API首先需要理解如下概念:
- REST:REST(Representational State Transfer)这个词,是Roy Thomas Fielding在他2000年的博士论文中提出的,翻译成中文大意为表现层状态传输。由于他是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席,所以REST原则迅速流行起来。当一个软件架构符合REST原则,我们称之为RESTful架构。说了这么多,我们为什么要使用RESTful架构?使用RESTful架构有什么好处?因为按照RESTful架构可以充分的利用HTTP协议带给我们的各种功能,算是对HTTP协议使用的最佳实践,还有一点就是可以使软件架构设计更加清晰,可维护性更好,但是并不是所有情况都需要完全遵守REST原则,毕竟实际情况远远比REST原则所定义的更加复杂,下面会详细介绍。
- 幂等性:幂等性(Idempotence)本身是一个数学概念,在HTTP/1.1规范中幂等性的定义是:
Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.
翻译过来大意就是如果方法调用一次和多次产生额外的效果是相同的,它就具有幂等性。
例子:在HTTP中使用GET方法通常用于从服务器获取资源,无论调用多少次产生的额外效果都是从服务器获取资源,所以GET具有幂等性;而POST方法通常用于提交数据在服务器上创建一个资源,由于最终创建的结果每次都是不同的,所以POST不具有幂等性;但是PUT方法却是幂等的,因为每次调用产生的效果都是对资源进行更新。 - 安全方法:安全方法是指不修改资源的 HTTP 方法。譬如,当使用 GET 或者 HEAD 作为资源 URL,都必须不去改变资源。然而,这并不全准确。意思是:它不改变资源的 表示形式。对于安全方法,它仍然可能改变服务器上的内容或资源,但这必须不导致不同的表现形式。
有关HTTP常用方法幂等性和安全性如下:
RESTful API设计规则:1. URI
- 应该将API部署在专用域名之下:https://api.example.com
- 不用大写
- 用中杠-不用下杠_;
- 参数列表要encode;
- URI中不应该出现动词,动词应该使用HTTP方法表示,但是如果无法表示,也可使用动词,例如:search没有对应的HTTP方法,可以在路径中使用search,更加直观;
- URI中的名词表示资源集合,使用复数形式;
- 虽然/在URI中表达层级,但是避免为了追求REST导致层级过深,适当使用参数表示。
GET /comments/tid?tid=1&page=1
2. Request:通过标准HTTP方法对资源CRUD
- GET:查询资源
GET /comments //获取所有评论 GET /comments/tid/1 //获取文章tid为1的所有评论
- POST:创建资源
POST /comments/tid/1 //为tid为1的文章创建评论
- PUT:更新资源
PUT /comments/cid/like/1 //为cid为1的评论点赞
- DELETE:删除资源
DELETE /comments/cid/1 //删除cid为1的评论
3. Response
- 采用JSON,不要使用XML
- 默认情况下JSON外层不需要嵌套大括号,API需要支持JSONP跨域访问或者客户端无法访问HTTP header才需要加上嵌套大括号
- 默认情况下不要过滤API输出中的空格,并且要支持gzip
4. API版本控制
- 在URI中存放:GET /v1/comments;
- 客户端在Accept Header中存放:Accept: application/vnd.github.v3+json,服务器自定义Header返回当前版本信息:X-GitHub-Media-Type: github.v3; format=json(GitHub在用);
- 以上两种方法根据情况选择,Github用的方式是REST中所要求的方式;
- 测试API和正式API要进行区分,方式通过如上两种方式实现。
5. 速度限制
为了避免请求泛滥,给API设置速度限制很重要。为此 RFC 6585 引入了HTTP状态码429(too many requests)。加入速度设置之后,应该提示用户,至于如何提示标准上没有说明,不过流行的方法是使用HTTP的返回头。
下面是几个必须的返回头(依照twitter的命名规则):
- X-Rate-Limit-Limit :当前时间段允许的并发请求数
- X-Rate-Limit-Remaining:当前时间段保留的请求数。
- X-Rate-Limit-Reset:当前时间段剩余秒数
为什么使用当前时间段剩余秒数而不是时间戳?
时间戳保存的信息很多,但是也包含了很多不必要的信息,用户只需要知道还剩几秒就可以再发请求了这样也避免了clock skew问题。
6.缓存
HTTP提供了自带的缓存框架。你需要做的是在返回的时候加入一些返回头信息,在接受输入的时候加入输入验证。基本两种方法:
- ETag:当生成请求的时候,在HTTP头里面加入ETag,其中包含请求的校验和和哈希值,这个值和在输入变化的时候也应该变化。如果输入的HTTP请求包含IF-NONE-MATCH头以及一个ETag值,那么API应该返回304 not modified状态码,而不是常规的输出结果。
- Last-Modified:和etag一样,只是多了一个时间戳。返回头里的Last-Modified:包含了RFC 1123 时间戳,它和IF-MODIFIED-SINCE一致。HTTP规范里面有三种date格式,服务器应该都能处理。
7.覆盖HTTP方法
一些HTTP客户端只支持GET和POST请求。为了能够加强这些客户端的访问能力,API需要能够覆盖HTTP方法。尽管这里没有任何强制的标准,但流行的做法是API会接收一个请求头X-HTTP-Method-Override,它的值可以是PUT、PATCH或者DELETE三者之一。
注意,用来覆盖HTTP方法的header只能在POST请求中被接受。GET请求永远不能修改服务器上的数据。
8.过滤信息
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数:
?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&per_page=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animal_type_id=1:指定筛选条件
9.错误处理
就像HTML的出错页面向访问者展示了有用的错误消息一样,API也应该用之前熟悉易读的格式来提供有用的错误消息。错误的表现形式应该跟其他资源保持一致,只是用一些自己的字段。
API应该一直返回合理的HTTP状态码。API错误一般情况下分成两类:代表客户端错误的400系列状态码和代表服务端错误的500系列状态码。API至少把所有400系列错误统一用易读的JSON格式来展示。如果可能(比如,如果负载均衡和反向代理能够创建自定义错误内容的话),500系列的状态码也这么弄。
JSON错误内容应该为开发者提供一些东西 – 有用的错误消息,唯一的错误码(通过它可以在文档中找到更多错误细节),可能的话提供错误细节描述。用JSON格式来输出错误看起来这样:
{
"code" : 1234,
"message" : "Something bad happened :(",
"description" : "More details about the error here"
}
对于PUT、PATCH和POST的请求进行的校验错误需要嵌套多个字段。最佳做法是用固定的错误码来表示校验失败,然后在额外的errors字段中提供错误的细节,像这样:
{
"code" : 1024,
"message" : "Validation Failed",
"errors" : [
{
"code" : 5432,
"field" : "first_name",
"message" : "First name cannot have fancy characters"
},
{
"code" : 5622,
"field" : "password",
"message" : "Password cannot be blank"
}
]
}
10.HTTP状态码
HTTP定义了很多有意义的状态码,你可以在你的API中使用。这些状态码可以帮助API消费者用来路由它们获取到的响应内容。整理了一个你肯定会用到的状态码列表:
- 200 OK – 对成功的GET、PUT、PATCH或DELETE操作进行响应。也可以被用在不创建新资源的POST操作上
- 201 Created – 对创建新资源的POST操作进行响应。应该带着指向新资源地址的Location header)
- 204 No Content – 对不会返回响应体的成功请求进行响应(比如DELETE请求)
- 304 Not Modified – HTTP缓存header生效的时候用
- 400 Bad Request – 请求异常,比如请求中的body无法解析
- 401 Unauthorized – 没有进行认证或者认证非法。当API通过浏览器访问的时候,可以用来弹出一个认证对话框
- 403 Forbidden – 当认证成功,但是认证过的用户没有访问资源的权限
- 404 Not Found – 当一个不存在的资源被请求
- 405 Method Not Allowed – 所请求的HTTP方法不允许当前认证用户访问
- 410 Gone – 表示当前请求的资源不再可用。当调用老版本API的时候很有用
- 415 Unsupported Media Type – 如果请求中的内容类型是错误的
- 422 Unprocessable Entity – 用来表示校验错误
- 429 Too Many Requests – 由于请求频次达到上限而被拒绝访问
11.认证
RESTful API应该是无状态。这意味着对请求的认证不应该基于cookie或者session。相反,每个请求应该带有一些认证凭证。
如果一直使用SSL,认证凭证可以简单的使用随机生成的access token,把其做为HTTP Basic Auth中user name字段的值传给API。这么做的好处是可以通过浏览器访问 – 如果浏览器从服务器收到401 Unauthorized状态码,它将会弹出一个对话框让人输出认证凭证。
当然,这种基于token来进行基本认证的方法只能当用户从API管理后台拷贝了一个token到自己的代码中才行。如果搞不到token,只能使用OAuth 2来把安全token传递给第三方。OAuth 2使用Bearer token,并且也是基于SSL来保证传输安全。
支持JSONP的API可能需要第三种方法来实现认证,因为JSONP的请求没法发送HTTP Basic Auth凭证或者Bearer token。这种情况下,可以使用一个额外的查询参数access_token。注意:使用查询参数来传递token存在一个固有的安全隐患,因为大多数web服务器会在服务器日志中保存查询参数。
不管怎么样,以上三种方法是用来在API之间传输token的方法。实际传输的token可以是一样的。
12.使用SSL
一定要使用SSL。没有例外。如今,你的web API可以从任何有互联网的地方(像图书馆,咖啡馆,机场等等)被访问到。这些地方并不都是安全的。很多地方根本没有对网络连接进行加密,如果认证凭证被劫持的话,这样访问者很容易被窃听或者被冒充。
一直使用SSL的另一个优势是,加密的连接简化了用户认证的工作 – 你可以使用简单的access token,而不需要对每个API请求进行签名。
需要注意的一件事是以非SSL的形式访问API的URL。不要把请求跳转到它们的SSL版本上。直接抛出一个严重错误!
13.Hypermedia API
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
比如,当用户向http://api.example.com的根目录发出请求,会得到这样一个文档。
{"link": {
"rel": "collection https://www.example.com/comments",
"href": "https://api.example.com/comments",
"title": "List of comments",
"type": "application/vnd.yourformat+json"
}}
上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。
Hypermedia API的设计被称为HATEOAS。
在进行分页查询时可以返回下一页的URI,如果没有说明服务器已经取到最后一条数据了,客户端可以减少不必要的请求以及URI的构造,建议在分页的情况下使用。
这就是我总结出来的RESTful API的最佳设计实践,文章如果有纰漏,欢迎指出。