RESTful API设计总结

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

规范

  1. 全部使用小写
  2. 一般url只能包含名词,如果某个资源动作非CURD,通过在url最后添加动词来标示动作,具体请看**#非CURD动作**。
  3. 非特殊情况名词使用复数,如分类应当表示为:categories,而不是category
  4. 资源名称使用蛇形命名,下划线隔开;参数字段名称同样。

资源表示

一般资源可以如下表示:

以下的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的响应。也能够用于一次未产生创建活动的POST
  • 201 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
    

转载于:https://my.oschina.net/crohn/blog/745658

你可能感兴趣的:(RESTful API设计总结)