Skip to content

概念

REST

REST 全称是 Representational State Transfer,中文意思是表述(通常译为「表征」)性状态转移。是一种网络应用程序的设计风格和开发方式,基于 HTTP,可以使用 XML 格式定义或 JSON 格式定义。

原则条件

REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful

当客户端通过 RESTful API 提出请求时,它会将资源状态表述传递给请求者或终端。该信息或表述通过 HTTP 以下列某种格式传输:JSON(Javascript 对象表示法)、HTML、XLT、Python、PHP 或纯文本。

还有一些需要注意的地方:头和参数在 RESTful API HTTP 请求的 HTTP 方法中也很重要,因为其中包含了请求的元数据、授权、统一资源标识符(URI)、缓存、cookie 等重要标识信息。有请求头和响应头,每个头都有自己的 HTTP 连接信息和状态码。

特点

RESTFUL 特点包括:

  1. 每一个 URI 代表 1 种资源;
  2. 客户端使用 GETPOSTPUTDELETE 4 个表示操作方式的动词对服务端资源进行操作:GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源;
  3. 通过操作资源的表现形式来操作资源;
  4. 资源的表现形式可以是 XML/HTML/JSON 等;
  5. 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。

简单来说,就是:用 URL 定位资源,用 HTTP 动词(GET: 获取, POST: 新建, DELETE: 删除, PUT: 更新)描述操作

接口路径设计

接⼝设计原则

URI 指向的是唯⼀的资源对象

示例:指向 ID 为 yanbo.ai 的 Account 对象

http
GET http://~/$version/accounts/yanbo.ai

URI 可以隐式指向唯⼀的集合列表

示例:隐式地指向 trades list 集合

http
GET http://~/$version/trades/(list)
等同于
GET http://~/$version/trades

聚合资源必须通过⽗级资源操作

示例: Profile 是 User 的聚合资源,User 有⼀个唯⼀且私有的 Profile 资源,只能通过 User 操作 Profile

http
更新 user_id 为 123456 的 Profile 资源
PUT http://~/$version/users/123456/profiles

Request Body:
{
    "full_name": "yanbo.ai",
    "state": "Shanghai",
    "title": "Senior software engineer"
}

组合资源要避免资源路径嵌套

建议不要超过两级嵌套,如果超过两级嵌套,请把相关请求条件转化为 Query 参数。

示例:⼀个系统⾥⾯包含多个 applications,⼀个 application ⼜包含多个 users。那获取 user 资源的路径应该是怎样的?看⼀个路径嵌套的例⼦:

http
GET http://~/$version/systems/:systemId/applications/:applicationId/users/:userId
http
GET http://~/$version/systems/:systemId
GET http://~/$version/applications/:applicationId
GET http://~/$version/users/:userId/

Http Methods

HTTP OperationDescription
GET获取,查找
POST新增创建
PUT更新
PATCH部分更新
DELETE删除

URL 组成

  1. ⽹络协议(HTTP/HTTPS)
  2. 服务器地址
  3. 版本
  4. 接⼝名称
  5. ? 参数列表
http
GET https://github.com/v1/trades

为什么需要版本?

当服务被更多其他系统使⽤的时候,服务的可⽤性和上下兼容变得⾄关重要。被外部系统依赖的服务在升级时是⼀个⾮常麻烦的事情,既要发布新的接⼝,⼜要保留旧的接⼝留出时间让调⽤者去升级。在 URL 中加⼊ Version 标示能很好地解决上下兼容(新⽼版本共存)问题。

示例 1:URL 中新增了 Path parameter

v1 版本:

http
GET http://~/v1/trades?user_id=123456

v2 版本:

http
GET http://~/v2/:user_id/trades

示例 1 中的 user_id 参数在 v2 版本被加⼊到 path parameter 中,使⽤ $version 保证了 v1 和 v2 接⼝的共存。

示例 2:数据接⼝发⽣变化

v1 版本:

http
GET http://~/v1/accounts/yanbo.ai

Response Body:
{
    "user_name": "yanbo.ai",
    "e_mail": "yanbo.ai@gmail.com",
    "state": "Shanghai",
    "title": "Senior software engineer"
}

v2 版本:

http
GET http://~/v2/accounts/yanbo.ai

Response Body:
{
    "user_name": "yanbo.ai",
    "e_mail": "yanbo.ai@gmail.com",
    "profile": {
        "state": "Shanghai",
        "title": "Senior software engineer"
    }
}

示例 2 中的接⼝返回数据结构已经发⽣了变化。使⽤ $version 保证了 v1 和 v2 接⼝的共存。

URL 定义限制

  1. 不使⽤⼤写字⺟
  2. 使⽤中线 - 代替下划线 _
  3. 参数列表应该被 encode 过

接⼝分类

资源对象的 CURD 操作

http
GET http://~/$version/trades            获取 trades 列表
GET http://~/$version/trades/:id        根据 id 获取单个 trade
POST http://~/$version/trades           创建 trade
PUT http://~/$version/trades/:id        根据 id 更新 trade
PATCH http://~/$version/trades/:id      根据 id 部分更新 trade
DELETE http://~/$version/trades/:id     根据 id 删除 trade

服务型接⼝

使⽤ services 标识,根据服务的属性选择 http ⽅法。

http
http://~/services/$version/server-name

示例 1: 搜索

http
GET http://~/services/$version/search?q=filter?category=file

示例 2:任务队列操作

http
PUT http://~/services/$version/queued/jobs          往任务队列⾥⾯添加⼀个新的任务
DELETE http://~/services/$version/queued/jobs/:id   根据 id 删除任务

系统设置

使⽤ settings 标识,根据服务的属性选择 http ⽅法。

http
http://~/settings/$version/server-name

示例:更改界⾯语⾔环境

http
PUT http://~/settings/$version/gui/lang
{
    "lang": "zh-CN"
}

为什么需要区分?

  1. Microservices Microservices 是⼀个全新的概念,它主要的观点是将⼀个⼤型的服务系统分解成多个微型系统。每个微型系统都能独⽴⼯作,并且提供各种不同的服务。独⽴运⾏的特点使微型系统之间不会产⽣相互影响,其中的⼀个微型系统宕机并不会牵连到其他的微型系统。这种架构使分布式系统的节点数量⼤⼤提升。因为 RESTful 服务是⽆状态的,所以这种分解并不会带来 状态共享的问题。
  2. 路由规则(逻辑) 当我们需要对不同属性的接⼝做路由规则的时候,按功能划分接⼝是⼀个很好的⽅案。例如:我们要对系统设置接⼝设置增 加更严格的调⽤限制。

缓存

⽹络接⼝相对于堆栈接⼝来说数据传输极其不稳定,尽可能地减少数据传输不仅能控制这种⻛险还能减少流量。使⽤缓存还能有效地提⾼后台的吞吐量。

后台在响应请求时使⽤响应头 E-TagLast-Modified 来标记数据的版本,前台在发送请求时将数据版本通过请求头 If- None-Match 帮助后台判断缓存的使⽤。

Request Header:

http
If-None-Match: 2390239059405940

Response Header:

http
E-Tag: 2390239059405940
Last-Modified: 2014-04-05T14:30Z

安全

调⽤限制

为保证服务的可⽤性应对服务进⾏调⽤过载保护。

Response Headers:

http
X-RateLimit-Limit: 3000             调⽤量的最⼤限制
X-RateLimit-Reset: 1403162176516    调⽤限制重置时间
X-RateLimit-Remaining: 299          剩余的调⽤量

安全验证

RESTful 服务使⽤ Oauth2 的⽅式进⾏调⽤授权,使⽤ http 请求头 Authorization 设置授权码;必须使⽤ User-Agent 设置客户端信息,⽆ User-Agent 请求头的请求应该被拒绝访问。

Request Header:

http
User-Agent: Data-Server-Client
Authorzation: Bearer 383w9JKJLJFw4ewpie2wefmjdlJLDJF

为什么建议使⽤ Oauth2 授权?

Oauth2 的参与者为:客户端,资源所有者,授权服务器,资源服务器。客户端先从资源所有者得到授权码,之后使⽤授权码从授权服务器得到 token,再使⽤ token 调⽤资源服务器获取经过资源所有者授权使⽤的资源。这种授权⽅式的特点有:

  1. 资源所有者可以随时撤销授权许可
  2. 可以通过撤销 token 拒绝客户端的调⽤
  3. 资源服务器可以拒绝客户端的调⽤

通过这三种⽅式可以做到对资源的严格保护。资源的访问权限也把握在资源所有者的⼿中,⽽不是资源服务器。当然,Oauth2 授权框架也允许受信任的客户端直接使⽤ token 调⽤资源服务器获取资源。这种灵活性完全取决于客户端类型和对资源的保护程度。

为什么授权码要放在 Http Header 中?

  1. WEB 服务器对访问做记录已经成为了⾏业的⼀个标准,访问记录不仅可以⽤来做访问量统计还能⽤来做访问特征分析。互联⽹⼴告平台就是利⽤访问记录来做精准营销的。如果 token(授权码) 包含在 URL 中就有很⼤的安全⻛险。
  2. 包含在 URL 中的 token 串可能被进⾏重定向传递。通过这两种⽅式⼊侵者可以不通过授权⽽使⽤泄漏的授权码访问那些受保护的数据,会造成数据泄漏的⻛险。

以 Tomcat 为例,访问⽇志为:

http
127.0.0.1 - - [24/Jun/2014:14:38:04 +0800] "GET /v1/accounts/yanbo.ai?token=dgdreLJLiuysdifyTsdofu

通过对访问⽇志的提取,很容易得到 token 信息。

数据设计

交互原则

  1. 查询,过滤条件使⽤ query string
  2. ⽤来描述数据或者请求的元数据放 Header 中,例如 X-Result-Fields
  3. Content body 仅仅⽤来传输数据
  4. 数据要做到拿来就可⽤的原则,不需要「拆箱」的过程
  5. 使⽤ ISO-8601 格式表达时间字段,例如: 2014-04-05T14:30Z

结构

使⽤ JSON 格式传输数据,在 http 请求头和响应头申明 Content-Type。返回的数据结构应该做到尽可能简单,不要过于包装。响应状态应该包含在响应头中。

Request:

http
Accept: application/jsonContent-Type: application/json;charset=UTF-8

Response:

http
Content-Type: application/json;charset=UTF-8

错误的做法:

http
{
    "status": 200,
    "data": {
        "trade_id": 1234,
        "trade_name": "Bala bala"
    }
}

正确的做法:

http
Response Headers:
    Status: 200
Response Body:
    {
        "trade_id": 1234,
        "trade_name": "Bala bala"
    }

示例:创建 User 对象

http
POST http://~/$version/users
Request
    headers:
        Accept: application/json
        Content-Type: application/json;charset=UTF-8
    body:
        {
            "user_name": "Andy Ai"
        }
Response
    status: 201 Created
    headers:
        Content-Type: application/json;charset=UTF-8
    body:
        {
            "uri": "http://~/$version/users/1234",
            "identity": 1234,
            "created_at": "2014-04-05T14:30Z",
            "links": [
                {
                    "rel": "next",
                    "href": "http://~/gui/users/1234"
                }
            ]
        }

为什么是 JSON?

JSON 是⼀种可以跨平台⾼扩展的轻量级的数据交换格式。易于⼈阅读和编写,同时也易于机器解析和⽣成。

属性定义限制

  1. 不能使⽤⼤写(⼤⼩写友好)
  2. 使⽤下划线 _ 命名(连接两个单词)
  3. 属性和字符串值必须使⽤双引号 ""

提取部分字段

⽆状态服务器应该允许客户端对数据按需提取。在请求头使⽤ X-Result-Fields 指定数据返回的字段集合。 例如:trade 有trade_id, trade_name, created_at 三个属性,客户端只需其中的 trade_id 与 trade_name 属性。

Request Header:

http
X-Result-Fields: trade_id,trade_name

⼦对象描述

数据⾥⾯的⼦对象使⽤ URI 描述不应该被提取,除⾮⽤户指定需要提取⼦对象。

示例:trade ⾥⾯的 order 对象

错误的做法:

http
{
    "trade_id": "123456789",
    "full_path": null,
    "order": {
        "order_id": "987654321"
    }
}

正确的做法:

http
{
    "trade_id": "123456789",
    "order": "http://~/$version/orders/987654321"
}

应⽤指定提取⼦对象,需要在请求头声明 X-Expansion-Fields

Request:

http
X-Expansion-Fields: true

为什么要客户端指定提取⼦对象时才提取?

懒模式服务能够最⼤程度地节省运算资源。虽然与客户端交互的次数有所增加,但是能做到按需提取,按需响应,这也是响应式设计的⼀⼤特点。客户端的⽤户⾏为模式⽆法真实地模拟,也就⽆法确定哪些资源需要做到⼀次性推送,让客户端按需使⽤是⼀个不错的⽅式。

关于空字段

应该在返回结果⾥⾯剔除空字段,因为 null 值传输到客户端并没有实际的含义,反⽽增加了占⽤空间。

Tips

使⽤ HTTP Header 时,优先使⽤合适的标准头属性。⽤ X- 作为前缀⾃定义⼀个头属性,例如:X-Result-Fields

状态码&错误处理

应⽤状态码

CodeHTTP OperationBody ContentsDescription
102 ProcessingGET, POST, PUT, DELETE, PATCH处理状态的信息当前请求正在处理
200 OkGET, PUT资源操作成功
201 CreatedPOST, PUT资源,元数据对象创建成功
202 AcceptedPOST, PUT, DELETE, PATCH处理信息请求已经被接受
204 No ContentDELETE, PUT, PATCHN/A操作已经执⾏成功,但是没有返回数据
301 Moved PermanentlyGETlink资源已被移除
303 See OtherGETlink重定向
304 Not ModifiedGETN/A资源没有被修改
400 Bad RequestGET, POST, PUT, DELETE, PATCH错误提示参数列表错误(缺少,格式不匹配)
401 UnauthorizedGET, POST, PUT, DELETE, PATCH错误提示未授权
403 ForbiddenGET, POST, PUT, DELETE, PATCH错误提示访问受限,授权过期
404 Not FoundGET, POST, PUT, DELETE, PATCH错误提示资源,服务未找到
405 Method Not AllowedGET, POST, PUT, DELETE, PATCH错误提示不允许的http⽅法
406 Not AcceptableGET, POST, PUT, DELETE, PATCH错误提示媒体内容不符合要求
408 Request TimeoutGET, POST, PUT, DELETE, PATCH错误提示请求超时
409 ConflictGET, POST, PUT错误提示资源冲突,重复的资源
415 Unsupported MediaType GET, POST, PUT, DELETE, PATCH错误提示不⽀持的数据(媒体)类型
422 Unprocessable EntityGET, POST, PUT, PATCH错误提示请求格式正确,但是由于含有语义错误,⽆法响应
423 LockedGET, POST, PUT, DELETE, PATCH错误提示当前资源被锁定
429 Too Many RequestsGET, POST, PUT, DELETE, PATCH错误提示请求过多被限制
500 Internal Server ErrorGET, POST, PUT, DELETE, PATCH错误提示系统内部错误
501 Not ImplementedGET, POST, PUT, DELETE, PATCH错误提示接⼝未实现

容器状态码

容器状态码是指 http 容器的状态码,应⽤不应该使⽤或限制使⽤

CodeHTTP OperationBody ContentsDescription
303GETlink静态资源被移除,应⽤限制使⽤
503GET, POST, PUT, DELETE, PATCHtext body服务器宕机
  • 4 开头的错误⽤来表达来⾃于客户端的错误,例如:未授权,参数缺失
  • 5 开头的错误⽤来表达服务端的错误,例如:在连接外部系统(DB)发⽣的 IO 错误

错误信息格式

错误信息应该包含下列内容:

  1. 错误标题 message,必须
  2. 错误代码 error code,必须
  3. 错误信息 error message,必须
  4. 资源 resource,可选
  5. 属性 field,可选
  6. ⽂档地址 document,可选

注意:Error Code 尽可能做到简洁明了,提取异常的关键字并且使⽤下划线 _ 把它们连接起来。

示例:调⽤频率超过限制

Response:

http
Headers:
    Content-Type: application/json;charset=UTF-8
    X-RateLimit-Limit: 3000
    X-RateLimit-Reset: 1403162176516
    X-RateLimit-Remaining: 0

{
    "message": "Message title",
    "errors": [
        {
            "code": "rate_limit_exceeded",
            "message": "Too Many Requests. API rate limit exceeded",
            "document": "https://developer.github.com/v3/gists/"
        }
    ]
}

锦上添花

  1. 格式化(Pettyprint)JSON 数据(返回结果)并且使⽤ gzip 压缩,Pettyprint 易于阅读,多余的空格在经过 gzip 压缩之后占 ⽤空间⽐压缩之前更⼩。
  2. 重写 Server
  3. 返回 X-Powered-By

Response Headers

http
X-Pretty-Print: true
Content-Encoding: gzip
Server: ods@shuyun.com
X-Powered-By: yanbo.ai;email=yanbo.ai@gmail.com

参考资料