【REST】RESTful API 浅谈

2019-07-26  本文已影响0人  佐蓝Gogoing

注:本文纯属个人理解的瞎扯淡,本人没有看过 Roy Thomas Fielding 关于 REST 的 论文

1. 望文生义 RESTful API

RESTful APIRESTful 有一个 -ful 词缀,明显的,它是一个形容词。
前面的 REST(Representational State Transfer) 就是个名词,百度翻译为:表述性状态转移

表述性状态转移这个词不好理解,因为在中文里没有这么说的,但是既然他是个形容词,那么 RESTful API 强行翻译一下就是:表述性状态转移的接口,在这里名词作形容词太拗口,准确来说应该是表述性状态转移那样的接口,那么问题来了,这不就等于没说吗?其实也不是,比如我们描述某个人,会说:那帅样、那死样,说的就是帅帅的那样的、死气沉沉那样的,是不是和前面说的很像,只是因为我们的语言习惯中没有词缀,而且默认转移这个词不是动词就是名称,那么就让我们强行把它认作形容词,一旦接受这个设定,好像就不是那么难以理解了。

但是强行认作形容词总感觉有点怪怪的,于是就有人把 REST 来源文章标题中的 style 拿过来,翻译成表述性状态转移风格的接口,这么一来就顺多了。但是 REST 本身就是原文中说到的那种软件体系结构风格的一种实现,所以我认为约束一词能更好的描述它,这样一来就成了符合表述性状态转移约束的接口,而这个约束是根据 Roy Thomas Fielding 在论文中所描述的风格制定的。

现在明白了 RESTful API 是符合某种约束的接口,那么这个约束是什么?这就要看表述性状态转移是什么了。我们可以把它拆开,变成 表述性 状态 转移,其中,转移是状态的转移,状态是以某种表述形式转移的。前面说到那帅样、那死样指的是人,那个这里是谁的状态在转移?

是资源,以我们习惯的主谓宾形式讲,就是以某种表述形式转移资源的状态,那么 RESTful API 就是以某种表述形式转移资源状态的接口。其中表述形式可以是 JSON、XML 等任何形式,状态在我理解即资源本身,因为事物的状态信息就是它自己(我也不知道从哪听说的 0.0)。这时会注意到上面我说 RESTful 是形容词,为什么又变成了以某种表述形式转移资源状态这样的动词了,这只是英文和中文在表述上的不同,中文中形容词后面往往跟着一个字:的,所以这个的是不能分开的,以某种表述形式转移资源状态的就是形容词啦。

接下来就需要讨论一下风格和约束了。

首先是风格,其在新华词典中的解释为:某一时期流行的一种艺术形式,这也很好地诠释了 REST 的目的:一种在未来十年或更久,不会过时的服务接口。就是说 REST 终究会过时,但他目前很流行。
风格是宽泛且多变的,拿服装举例,一个时代、一个民族、一个流派、一种性格、一种价值取向等都会衍生出一种服装风格,像是常服、旗袍、晚宴装、Lolita 等。在 Lolita 这一风格中又会有甜美、古典、哥特、庞克、和风、田园等风格,在风格的风格中,又会有各种各样的表现形式。
那么这里的风格是什么?上面说到,转移的是资源的状态,所以我认为这里的风格就是:以资源为中心

然后是约束,约束不同于规范,它是在一定的范围内有自由空间的,约束不会说你只能干什么,它只会说你不能干什么,比如法律和道德就是约束。法律由国家制定,道德由社会发展自然形成,在 REST 中,约束由使用者自己定义,只要是以资源为中心风格的就可以。
但是有一个约束是自定义约束都要包括的,因为状态只是在不停的转移,它没有变,所以这个约束就是:资源状态在转移过程中不能发生改变
有人会说新增、删除就改变了状态啊。是的,新增、删除肯定会改变资源状态,但是在我理解中,新增、删除这种改变资源状态的,本身就不是转移,所以不符合这个约束也是正常的,但是我们可以将这些操作的形式,在 API 表现上设计成和转移一样,这样看上去就很顺眼了。

注意:只要 API 用的舒服,代码敲得舒心,不需要为了 REST 而 REST。如果你觉得 REST 使你快乐,那就 REST,如果你觉得 REST 没什么好处,反而是一大堆限制,那就不要 REST,毕竟人生苦短,开心就好。

2. HTTP RESTful API

上面说到 RESTful API 是一种风格的约束,所以 HTTP RESTful API 是 RESTful API 的一种实现。除了 HTTP,也可以用 MQTT、OpenWire、WebSocket 等协议实现这种约束。但是因为 B/S 架构使用非常广泛,目前所说的 RESTful API 基本都是 HTTP 上的实现。

本章照搬 RESTful 接口最佳实践。

2.1 接口路径设计

2.1.1 接口设计原则

  1. URI指向的是唯一的资源对象

示例: 指向ID为yanbo.aiAccount对象

GET http://~/$version/accounts/yanbo.ai
  1. URI可以隐式指向唯一的集合列表

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

GET http://~/$version/trades/(list)
等同于
GET http://~/$version/trades
  1. 聚合资源必须通过父级资源操作

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

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

Request Body:
{
    "full_name": "yanbo.ai",
    "state": "Shanghai",
    "title": "Senior software engineer"
}
  1. 组合资源要避免资源路径嵌套

看一个路径嵌套的例子

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

这样做是不合理的,它会让你的接口变得越来越混乱和缺少灵活性。正确的做法是:

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

2.1.2 Http Methods

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

2.1.3 URL组成

  1. 网络协议(HTTP, HTTPS)
  2. 服务器地址
  3. 版本
  4. 接口名称
  5. ?参数列表

为什么需要版本?
当服务被更多其他系统使用的时候,服务的可用性和上下兼容变得至关重要。被外部系统依赖的服务在升级时是一个非常麻烦的事情,既要发布新的接口,又要保留旧的接口留出时间让调用者去升级。在URL中加入Version标示能很好地解决上下兼容(新老版本共存)问题。

示例1: URL中新增了Path parameter
v1版本

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

v2版本

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

示例1中的user_id参数在v2版本被加入到path parameter中,使用$version保证了v1v2接口的共存。

示例2: 数据接口发生变化
v1版本

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版本

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保证了v1v2接口的共存。

2.1.4 URL定义限制

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

2.1.5 接口分类

资源对象的CURD操作

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://~/services/$version/server-name

系统设置
使用settings标识,根据服务的属性选择http方法。

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

示例1: 搜索

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

示例2: 任务队列操作

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

示例3: 更改界面语言环境

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

为什么需要区分?

  1. Microservices
    Microservices是一个全新的概念,它主要的观点是将一个大型的服务系统分解成多个微型系统。每个微型系统都能独立工作,并且提供各种不同的服务。独立运行的特点使微型系统之间不会产生相互影响,其中的一个微型系统宕机并不会牵连到其他的微型系统。这种架构使[分布式系统的节点数量][6]大大提升。因为RESTful服务是无状态的,所以这种分解并不会带来状态共享的问题。

  2. 路由规则(逻辑)
    当我们需要对不同属性的接口做路由规则的时候,按功能划分接口是一个很好的方案。例如:我们要对系统设置接口设置增加更严格的调用限制。

2.1.6 缓存

网络接口相对于堆栈接口来说数据传输极其不稳定,尽可能地减少数据传输不仅能控制这种风险还能减少流量。使用缓存还能有效地提高后台的吞吐量。
后台在响应请求时使用响应头E-TagLast-Modified来标记数据的版本,前台在发送请求时将数据版本通过请求头If-None-Match帮助后台判断缓存的使用。

Request Header

If-None-Match: 2390239059405940

Response Header

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

2.1.7 Bookmarker

在实际的环境中,有大量的查询需求是相同的。将这些搜索需求标签化能降低使用难度也可以达到重用的目的。

示例1: 查找状态为关闭的订单
普通方式

GET http://~/$version/trades?status=closed&sorting=-created_at

Bookmarker

GET http://~/$version/trades#recently_closed

GET http://~/$version/trades/recently_closed

2.1.8 HATEOAS

HATEOAS通过Web Linking的方式来描述程序的状态信息
Link 主要包含以下属性:

Property Description
rel 关联内容
href URL
type 媒体类型
method Http Method
title 标题
arguments 参数列表
value 返回值

Rel 可能为以下值:

Value Description
next 下一步
prev 上一步
first 第一步,最前
last 最后一步,最后
source 来源
self 资源自身,相对于this

Web Linking 可以通过两种方式传递至客户端:
Http Header

Link: <http://~/$version/trades?page_no=10>; rel="next", <http://~/$version/trades?page_no=19>; rel="last"

Http JSON Body

{
    "links": [
        {
            "rel": "next",
            "href": "http://~/$version/trades?page_no=1"
        },
        {
            "rel": "last",
            "href": "http://~/$version/trades?page_no=19"
        }
    ]
}

示例1: 用户注册业务

  1. 用户填写E-Mail与密码
  2. 完善用户资料

Register Request

POST http://~/$version/accounts
Headers:
    Accept: application/json
    Content-Type: application/json;charset=utf-8
Body:
    {
        "username": "yanbo.ai@gmail.com",
        "e_mail": "yanbo.ai@gmail.com",
        "password": "balabala"
    }

Register Response

Headers:
    Content-Type: application/json;charset=utf-8
Status: 201 Created
Body:
    {
        "uri": "http://~/$version/accounts/yanbo.ai",
        "identity": "yanbo.ai",
        "created_at": "2014-04-05T14:30Z",
        "links": [
            {
                "rel": "next",
                "href": "http://~/$version/accounts/yanbo.ai/profiles",
                "method": "POST",
                "title": "Editing Profiles",
                "arguments": "status=editing"
            }
        ]
    }

Profile Request

POST http://~/$version/accounts/yanbo.ai/profiles
Headers:
    Accept: application/json
    Content-Type: application/json;charset=utf-8
Body:
    {
        "full_name": "yanbo.ai",
        "state": "Shanghai",
        "title": "Senior software engineer"
    }

Profile Response

Headers:
    Content-Type: application/json;charset=utf-8
Status: 201 Created
Body:
    {
        "uri": "http://~/$version/accounts/yanbo.ai/profiles",
        "identity": "yanbo.ai",
        "created_at": "2014-04-05T14:30Z"
    }

HATEOAS在解决什么问题?
HATEOAS是Hypermedia as the Engine of Application State的缩写形式,中文意思为:超媒体应用状态引擎。它的核心思想是使用超媒体表达应用状态,与hypertext-driven思想是一致的。在此之前,我们大多数的程序业务控制在前台完成。例如:我们会在前台做注册流程,我们在前台判定下一步应该做什么,可以做什么。当使用HATEOAS时,这些状态流程控制都在应用程序的后台完成。我们使用超媒体来表达前台做完某一步骤之后可以做哪些? 这样一来,前台的任务就变得相当简单了,前台需要处理的是理解状态表述,数据收集和结果显示。

2.1.9 分页

Request

GET http://~/$version/trades?page=10&pre_page=100

Response
Link Header

Link: <http://~/$version/trades?page=11&pre_page=100>; rel="next", <http://~/$version/trades?page=19&pre_page=100>; rel="last"

JSON Body

{
    "links": [
        {
            "rel": "next",
            "href": "http://~/$version/trades?page=11&pre_page=100"
        },
        {
            "rel": "last",
            "href": "http://~/$version/trades?page=19&pre_page=100"
        }
    ]
}

2.2 安全

2.2.1 调用限制

为保证服务的可用性应对服务进行调用过载保护
Response Headers

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

2.2.2 安全验证

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

Request Header

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为例,访问日志为:

127.0.0.1 - - [24/Jun/2014:14:38:04 +0800] "GET /v1/accounts/yanbo.ai?token=dgdreLJLJLER798989erJKJK HTTPS/1.1" 200 343

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

2.3 数据设计

2.3.1 交互原则

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

2.3.2 结构

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

Request

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

Response

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

错误的做法

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

正确的做法

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

示例1: 创建User对象

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 是一种可以跨平台高扩展的轻量级的数据交换格式。易于人阅读和编写,同时也易于机器解析和生成。

2.3.3 属性定义限制

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

2.3.4 提取部分字段

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

Request Header

X-Result-Fields: trade_id,trade_name

2.3.5 子对象描述

数据里面的子对象使用URI描述不应该被提取,除非用户指定需要提取子对象
示例: ·trade·里面的·order·对象
错误的做法

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

正确的做法

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

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

Request

X-Expansion-Fields: true

为什么要客户端指定提取子对象时才提取?
懒模式服务能够最大程度地节省运算资源。虽然与客户端交互的次数有所增加,但是能做到按需提取,按需响应,这也是响应式设计的一大特点。客户端的用户行为模式无法真实地模拟,也就无法确定哪些资源需要做到一次性推送,让客户端按需使用是一个不错的方式。

关于空字段
应该在返回结果里面剔除空字段,因为null值传输到客户端并没有实际的含义,反而增加了占用空间。

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

2.4 状态码&错误处理

2.4.1 应用状态码

Code HTTP Operation Body Contents Description
102 Processing GET, POST, PUT, DELETE, PATCH 处理状态的信息 当前请求正在处理
200 Ok GET, PUT 资源 操作成功
201 Created POST, PUT 资源, 元数据 对象创建成功
202 Accepted POST, PUT, DELETE, PATCH 处理信息 请求已经被接受
204 No Content DELETE, PUT, PATCH N/A 操作已经执行成功,但是没有返回数据
301 Moved Permanently GET link 资源已被移除
303 See Other GET link 重定向
304 Not Modified GET N/A 资源没有被修改
400 Bad Request GET, POST, PUT, DELETE, PATCH 错误提示 参数列表错误(缺少,格式不匹配)
401 Unauthorized GET, POST, PUT, DELETE, PATCH 错误提示 未授权
403 Forbidden GET, POST, PUT, DELETE, PATCH 错误提示 访问受限,授权过期
404 Not Found GET, POST, PUT, DELETE, PATCH 错误提示 资源,服务未找到
405 Method Not Allowed GET, POST, PUT, DELETE, PATCH 错误提示 不允许的http方法
406 Not Acceptable GET, POST, PUT, DELETE, PATCH 错误提示 媒体内容不符合要求
408 Request Timeout GET, POST, PUT, DELETE, PATCH 错误提示 请求超时
409 Conflict GET, POST, PUT 错误提示 资源冲突,重复的资源
415 Unsupported Media Type GET, POST, PUT, DELETE, PATCH 错误提示 不支持的数据(媒体)类型
422 Unprocessable Entity GET, POST, PUT, PATCH 错误提示 请求格式正确,但是由于含有语义错误,无法响应。
423 Locked GET, POST, PUT, DELETE, PATCH 错误提示 当前资源被锁定
429 Too Many Requests GET, POST, PUT, DELETE, PATCH 错误提示 请求过多被限制
500 Internal Server Error GET, POST, PUT, DELETE, PATCH 错误提示 系统内部错误
501 Not Implemented GET, POST, PUT, DELETE, PATCH 错误提示 接口未实现

2.4.2 容器状态码

Code HTTP Operation Body Contents Description
303 GET link 静态资源被移除,应用限制使用
503 GET, POST, PUT, DELETE, PATCH text body 服务器宕机

Tips
4开头的错误用来表达来自于客户端的错误,例如: 未授权,参数缺失。5开头的错误用来表达服务端的错误,例如: 在连接外部系统(DB)发生的IO错误。

2.4.5 错误信息格式

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

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

Tips
Error Code 尽可能做到简洁明了,提取异常的关键字并且使用下划线_把它们连接起来。
示例: 调用频率超过限制,Response:

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/"
        }
    ]
}

2.5 锦上添花

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

Response Headers

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

参考

上一篇下一篇

猜你喜欢

热点阅读