RESTful API设计总结
基于RESTful API设计规范设计,按照实际情况适当调整细节。
目前个人较好的具体实践为,使用Laravel+dingo api+JWT的方式开发接口,文档基于swagger ui生成,遵循PSR2的开发规范,使用Chrome的DHC插件来测试接口。
注意:以下示例通常省略域名和版本号
通用说明
协议
使用http协议,如果有安全方面的担忧,或者数据异常重要可以使用https
编码
统一使用utf8编码
域名
当前使用独立的api二级域名:
http://api.host.com
如果已经存在二级域名,建议使用name-api.host.com的形式
版本控制
所有的版本号都放入url,并且使用标准版本号格式,前面加小写的v:
http://api.host.com/v1
版本号格式说明:通用的为主版本号.次版本号.修订版本号;当然,可以根据自己实际需求调整,针对某些小型项目,直接主版本号,不包含次版本号和修订版本号。版本号从v1开始。
响应数据格式
所有响应数据格式使用json格式
请求/Request
基于REST原则,一个url表示一个资源实体或资源实体的集合,对资源的操作使用动作(action)来完成,比如查询一个应用,添加一条数据等;同时使用参数来限制返回数据结果,比如分页,排序,查询条件等。
URL
规范
- 全部使用小写
- 一般url只能包含名词,如果某个资源动作非CURD,通过在url最后添加动词来标示动作,具体请看**#非CURD动作**。
- 非特殊情况名词使用复数,如分类应当表示为:categories,而不是category
- 资源名称使用蛇形命名,下划线隔开;参数字段名称同样。
资源表示
一般资源可以如下表示:
以下的id均表示资源的唯一标识,一般可以理解为数据库主键。
/applications //表示所有应用资源集合
/applications/1 //表示id为1的应用资源
但是我们有时候也需要表示子资源,或者说附属资源:
/applications/1/app_repositories //表示id为1的应用所属的所有仓库版本资源的集合
/applications/1/app_repositories/22 //表示id为1的应用所属的id为22的单个仓库版本资源
除了资源简单的CURD,服务器端经常还会提供其他服务。这些服务有的无法用实体资源表述(比如搜索),有的不能使用通用的动作来描述(如打星标)。对于这些服务,可以抽象成合适的资源,使用合适的动作去处理(*非CURD动作的处理方式可以看*动作(action)详述),如:
// 搜索
GET /search?q=filter?category=file
// 给ID为1的应用打上星标
POST /applications/star
Host: api.host.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
...
id=1
动作(action)
CURD
一个标准的RESTful对资源的操作使用HTTP方法来区分,如下:
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。
但是有的客户端有时候并不支持PUT,PATCH,DELETE方法,同时nginx默认也并不接受这些http请求。
所以,我们使用POST+__method参数的方式来伪造PUT,DELETE方法。使用PUT来更新,默认不使用PATCH方法。
-
GET - 查询
GET /applications //查找所有应用 GET /applications/1 //查找id为1的应用 GET /applications/1/app_repositories //查找id为1的应用的所有仓库版本数据
-
POST - 新增
POST /applications //新增应用 POST /applications/1/app_repositories //为ID为1的应用新增一个版本数据
-
PUT - 修改
// 修改ID为1的应用 POST /applications/1 Host: api.host.com Accept: application/json Content-Type: application/x-www-form-urlencoded ... // 请求POST数据,注意PUT大写 __method=PUT&user_id=1&category_id=22...
-
DELETE - 删除
// 删除ID为1的应用 POST /applications/1 Host: api.host.com Accept: application/json Content-Type: application/x-www-form-urlencoded ... // 请求POST数据 __method=DELETE //这里大写
非CURD动作
当一个操作并非CURD动作时,将动作抽象为一个资源,利用RESTful原则像处理子资源一样处理它,将动词加在url最末尾,如:
// 给ID为1的应用打上星标
POST /applications/star
Host: api.host.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
...
id=1
// 打回信息
POST /applications/back
Host: api.host.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
...
id=1
message=打回原因....
POST请求的内容参数格式
当POST请求时,一般会有3种内容格式:
- Content-Type: application/x-www-form-urlencoded (浏览器POST表单用的格式)
- Content-Type: application/json
- Content-Type: multipart/form-data; boundary=----XXXXXXXXXXXXXXXX
当前POST方法统一使用Content-Type: application/x-www-form-urlencoded格式,如果需要上传请使用Content-Type: multipart/form-data; boundary=----XXXXXXXXXXXXXXXX格式,例子如上所示。
参数
一个查询可以用参数来限制返回结果,一个修改当然也可以用参数来限制范围。以下针对查询参数说明(修改参数都是在body中的):
常用参数
-
fields 使用fields=x,x,x的形式限制返回的字段
GET /applications?fields=user_id,star //返回所有应用集合的user_id,star值
-
单字段条件查询 使用key=value的形式来附带查询条件
GET /applications?star=true //返回打上星标的应用集合
-
排序 使用sort=+/-field,+/-field的形式来排序,+表示升序排列,-表示降序排列,默认为+,可以用逗号隔开多个排序规则
GET /applications?sort=-update_at //返回按照更新时间由大到小的应用集合 GET /applications?sort=+update_at //返回按照更新时间由小到大的应用集合 GET /applications?sort=user_id,-create_at //先按照user_id升序排列,user_id一样就按照创建时间降序排列
-
分页 使用pre_page=10&page=3的形式来分页,pre_page表示一页多少条数据,page表示页数
GET /applications?pre_page=15&page=1 //按照一页15条数据分页,返回所有应用集合的第一页
可选参数
-
可选的附带返回相关资源 有时候需要返回一个资源和多个相关资源,如果使用多个接口去请求效率较低,所以可以使用embed=app_repositories.name,users的形式返回需要的相关资源,并指定资源返回的属性(字段)。但是注意这种方式很可能会增加复杂度,所以一般不太推荐使用,可以使用下面的封装请求。
“embed”将是一个逗号隔开的需要被内置的相关资源列表,默认返回所有字段。点号可以用来指定字段,类似fields的作用。
// 请求 GET /applications/12?fields=id,user_id,category_id&embed=app_repositories.name,users // 响应 { "id" : 12, "user_id" : 2, "category_id" 3, "app_repositories" : { "name" : "应用名" }, "users": { "id" : 42, "name" : "admin", } }
封装请求
如果某些请求url很长,或者我需要获取多个资源并一起返回,那么可以封装成常用请求url;建议使用短横线-来标示。如:
// 获取平台类型为4,打上星标的所有已审核应用
GET /applications?star=true&audit=1&sort=create_at
to:
GET /applications/star-audit
// 同时获取所有应用资源集合和相关的用户资源,应用版本资源
GET /applications/12?embed=app_repositories,users
to:
GET /applications/12/app_repositories-users
响应/Response
响应结果全部使用json格式,当请求成功时返回200状态,当失败时返回非200状态,状态码不体现在返回数据中。
基本约束:
- 时间使用长整型表示时间戳,需要展示客户端自己计算
- 不返回null,空直接表示为"",[],{}
- 响应头内容类型全部为json,content-type:application/json
状态码说明
成功状态码
200 OK (成功)
- 对一次成功的GET, PUT或 DELETE的响应。也能够用于一次未产生创建活动的POST201 Created (已创建)
- 用户新建或修改数据成功。同时可以结合使用一个位置头信息指向新资源的位置(可选)204 No Content (没有内容)
- 对一次没有返回主体信息(像一次DELETE请求)的请求的响应
跳转状态码
304 Not Modified
- 资源自从上次请求后没有再次发生变化,主要使用场景在于实现数据缓存。如请求资源时带上请求头If-Match,并且和客户端的Etag值一致,则直接返回304表示客户端可以使用缓存,并不返回实际内容。
客户端错误状态码
400 Bad Request (错误的请求)
- 请求错误, 比如无法解析请求体401 Unauthorized (未授权)
- 未登录的用户。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)403 Forbidden (禁止访问)
- 当认证成功但是认证用户无权访问该资源时404 Not Found (未找到)
- 当一个不存在的资源被请求时412 Precondition Failed
- 当请求头If-Match的值和客户端的Etag验证不一致时返回,或者其他并发控制出错返回429 Too Many Requests (请求过多)
- 当请求由于访问速率限制而被拒绝时
服务端错误状态码
500, 501, 502, 503, 等等
- 服务端错误,看情况返回响应体或者只返回状态码**(详情查阅接口文档)**
正确响应
当响应为200系列状态码的时候
单个资源返回一个json对象,可以用于查询和修改,新增
{
"field1": "value",
"field2": true,
"field3": [],
"field4": {}
}
GET 资源集合返回一个json数组对象
[
{
"field1": "value",
"field2": true,
"field3": []
},
{
"field1": "another value",
"field2": false,
"field3": []
}
]
错误响应
错误通常被分成两种类型: 代表客户端问题的400系列状态码和代表服务器问题的500系列状态码。
一个标准的错误信息应当包含以下内容:
status_code
一个标示唯一错误代码的编号message
错误信息description
可能的错误详细描述,这个可选
{
"status_code" : 404,
"message" : "没找到数据",
"description": "当前查询没有找到数据"
}
// 401
{
"status_code" : 401,
"message": "未登录或令牌错误"
"description": "验证没有通过,请重新请求令牌"
}
对PUT和POST请求进行错误验证将需要一个字段分解,使用一个固定的顶层错误代码来验证错误,并在额外的字段中提供详细错误信息,使用error
来提供字段错误信息
{
"status_code" : 400,
"message" : "验证失败",
"errors" : [
{
"field" : "name",
"message" : "名字长度必须小于30个字符"
},
{
"field" : "password",
"message" : "密码不能为空"
}
]
}
速率限制(可选)
防止滥用和无意义的高频率请求,需要对请求作出某些速度和频率限制。当前响应状态429 Too Many Requests (请求过多)
就是超过限制时候的响应状态码。
使用下列三个相应头来标明请求限制:
- X-Rate-Limit-Limit - 当前允许请求的总次数
- X-Rate-Limit-Remaining - 当前剩余的允许请求次数
- X-Rate-Limit-Reset - 当前允许请求的剩余秒数,也就是说多少秒之后才能重新请求
以上三个响应头按照实际情况使用,初期接口可以不实现。
认证和授权/Authentication and Authorization
Authentication
当前认证通用方式有三种:
- HTTP基本认证: 只有在部署了 SSL 证书的情况下才可以使用,否则用户密码会有暴露的风险
- JSON WEN TOKEN:支持通过登录接口使用账号密码获取,在请求接口时使用 Authorization: Bearer #{token} 头标或者 token 参数的值的方式进行验证。参考文章
- OAuth2:通用标准,认证授权机制都包含,具体可以参考官网或者这篇文章
我们当前使用JWT(JSON WEN TOKEN)方式来做认证和授权,如果有需要以后可以扩展为OAuth2的方式。
注意,所有的请求都需要申请Token,如果没有附带Token或者Token错误返回401 Unauthorized (未授权)
状态,并附带相关message信息。
服务端
当前服务端的laravel框架我们使用tymondesigns/jwt-auth扩展来实现,具体见文档,参考例子:
// 获取应用
GET /application?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL29wZW4tYXBpLmpmby5jb21cL3YxXC90b2tlbiIsImlhdCI6MTQ2NjkxMzQ3OSwiZXhwIjoxNDY2OTQ5NDc5LCJuYmYiOjE0NjY5MTM0NzksImp0aSI6Ijg0MzdjZjBjOTA2MDE3OGFkMWMzNDEyODVlY2VhNTUzIn0.wRECdhY1h8APcsX0qeYQjYEgM9z4efM_KjPzxGUDsvo
// 发布应用 token附带到url参数
POST /applications/publish?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL29wZW4tYXBpLmpmby5jb21cL3YxXC90b2tlbiIsImlhdCI6MTQ2NjkxMzQ3OSwiZXhwIjoxNDY2OTQ5NDc5LCJuYmYiOjE0NjY5MTM0NzksImp0aSI6Ijg0MzdjZjBjOTA2MDE3OGFkMWMzNDEyODVlY2VhNTUzIn0.wRECdhY1h8APcsX0qeYQjYEgM9z4efM_KjPzxGUDsvo
Host: api.host.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
...
id=1
POST请求的时候,token可以附带到url参数,也可以附带到请求头。如果使用请求头附带token则如下:
Authorization: Bearer {yourtokenhere}
// 发布应用 token附带到请求头
POST /applications/publish
Host: api.host.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL29wZW4tYXBpLmpmby5jb21cL3YxXC90b2tlbiIsImlhdCI6MTQ2NjkxMzQ3OSwiZXhwIjoxNDY2OTQ5NDc5LCJuYmYiOjE0NjY5MTM0NzksImp0aSI6Ijg0MzdjZjBjOTA2MDE3OGFkMWMzNDEyODVlY2VhNTUzIn0.wRECdhY1h8APcsX0qeYQjYEgM9z4efM_KjPzxGUDsvo
...
id=1
客户端
非laravel框架的客户端自己实现,具体实现见另外一篇文档。
Authorization
如果使用JWT,那么我们可以从Token出解析出user信息,然后每次检测这个user权限即可。当前我们选择此方式
如果使用OAth2,本身就附带授权机制。
数据缓存和并发控制(可选)
使用If-Match请求头和Etag响应头来维护并发修改的一致性,以及请求缓存机制。
具体实现为:
-
获取Etag 客户端请求某个资源的时候,响应中包含Etag的响应头,Etag是资源的唯一标签,每个资源都不同。具体到程序可以直接使用所有字段数据的数组计算hash值,如MD5。
// 请求 GET http://api.host.com/application/1 // 响应 HTTP/1.1 200 OK ... ETag: "644b5b0155e6404a9cc4bd9d8b1ae730" Content
-
缓存机制 当客户端重复请求同一个资源时,带上If-Match请求头,这个请求头的值即为上次请求此资源的Etag值。然后服务端检测这个值并和当前的服务端Etag比对,如果一致说明数据没有经过修改,返回
304 Not Modified
,表示可以使用客户端缓存,并不返回任何数据。 注意:对于某些客户端本身并没有缓存机制的时候,这个不能生效。比如PHP的CURL请求本身并没有缓存任何东西// 请求,这里用curl带上请求头 $ curl -i http://api.host.com/application/1 -H "If-Match: 644b5b0155e6404a9cc4bd9d8b1ae730" // 响应 HTTP/1.1 304 Not Modified
-
并发控制 当客户端想修改这个资源的时候,带上If-Match请求头,这个请求头的值即为最后一次请求此资源的Etag值。然后服务端检测这个值并和当前的服务端Etag比对,如果不一致,则返回
412 Precondition Failed
表示资源已经修改过了,请重新请求资源并确认修改。// 修改请求 POST /applications/1 Host: api.host.com Accept: application/json Content-Type: application/x-www-form-urlencoded If-Match: 644b5b0155e6404a9cc4bd9d8b1ae730 __method=PUT&user_id=1&category_id=22... // 响应 HTTP/1.1 412 Precondition Failed