关于REST风格

Web服务在过去很长一段时间内,都是使用RPC风格的SOAP(简单对象访问协议)来进行搭建,而最近几年,用REST风格搭建Web服务变得越来越流行。REST概念在被提出来的16年的时间里,也经过了大量的验证和优化。

REST是Representational State Transfer的缩写,Representational是展示层,是将Web资源进行展示分享处理的形式,State Transfer即状态转化,可以理解为在使用Web服务的时候,客户端与服务器端进行了交互,在交互的同时,数据以及状态会发生改变。

互联网通信中常用的超文本传输协议(HTTP),是一个无状态协议。所以在C/S的交互中,所有的资源状态都保留在服务器上,因此客户端需要使用一些策略来促使服务器发生状态的改变。客户端通常用到的策略是HTTP协议里四个表示操作方式的动作:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。还有一个不常用的动作 PATCH,作用也是在服务器更新资源,但是类型和PUT稍有不同,感兴趣的可以自行查找相关的资料。

因此REST风格可以理解为:客户端访问资源的位置(URI),通过主要的HTTP动作,操作服务器端资源,从而达到”展示层状态转化”。

关于API

在Web服务中,前端和后端是两个大部分。当前越来越流行的趋势是将前后端进行分离,在典型的MVC框架下,前端负责Controller和View,而后端专注于Model。因此我们需要一个行之有效而且统一方便的机制,来方便各种情况下前端和后端进行通信。

Web API(Application Programming Interface)构架因此孕育而生,基于 REST 风格的 API 也是目前较为成熟的Web App API 设计思想。

RESTful API的一些设计规范

1.规范的资源定位符

URI书写的基本规范有以下几点:

  1. 不用大写;
  2. 用中杠 - 不用下杠 _
  3. 参数列表要encode;
  4. URI中的名词表示资源集合,使用复数形式。

URI表示资源的两种方式:资源集合、单个资源。单个资源是资源集合中的单元,比如说/schools代表所有学校的资源集合,/schools/1代表的则是学校集合中的单个学校。在这样的设计下可能会出现/schools/1/grades/3/classes/25这样层级很深的URI,尽量使用查询参数代替路径中的实体导航, 这里我们可以优化成 /classes?school=1&grade=3/25

一般情况下,我们会将API放在指定的路由下,以方便在使用的时候做出区分,以下是常见的形式:

1
2
http://example.com/api
http://example.com/api/v1 #带了版本号

2.规范的请求

通过标准HTTP方法对资源CRUD:

GET

1
2
3
GET /schools
GET /schools/1
GET /schools/1/grades

POST:POST一般向“资源集合”型uri发起

1
2
POST /schools
POST /schools/1/grades

PUT/PATCH:更新单个资源(PUT全量/PATCH差量)

1
2
PUT /schools/1
PATCH /schools/1/grades/3

DELETE

1
2
3
DELETE /schools/1/grades/2
DELETE /schools/1/grades/2;4;5
DELETE /schools/1/grades #删除所有id为1的学校下的所有年级

在进行资源查询的时候,可以加入一些复杂查询的条件:

  1. 过滤条件?school=1&grade=3
  2. 排序 ?sort=class,desc
  3. 投影 ?whitelist=id,name,email
  4. 分页 ?limit=10&offset=3&page=3

请求体一般有application/json,application/x-www-form-urlencoded,multipart/form-data这些格式,对于返回体的格式,可以在请求的Header里面指定Accept的类型,也可以将文件类型加入到URI里面,比如类似 /schools/1.json

3.规范的返回内容

  • GET /schools:以数组形式返回所有资源的列表
  • GET /schools/1:返回单个资源对象
  • POST /schools:返回新生成的资源对象
  • PUT /schools/1:返回完整的资源对象
  • PATCH /schools/1:返回完整的资源对象
  • DELETE /schools/1:返回一个空文档

一般情况下,返回的内容不要包含资源以外的内容,譬如说请求状态等,因为返回的Header里面会有状态码,因此不需要多此一举。当然对于有特殊查询条件的比如说分页,可以有例外:

1
2
3
4
5
6
7
8
9
{
"paging":
{
"limit": 20,
"offset": 2,
"total": 100
},
"data":{...}
}

4.对状态码的处理规范

常见的HTML返回码都包含以下:

成功类型:

  • 200 OK - [GET]:成功返回数据
  • 201 CREATED - [POST/PUT/PATCH]:新建或修改数据成功
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功

异常类型:

  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误
  • 401 Unauthorized - [*]:表示用户没有权限
  • 403 Forbidden - [*] 表示用户得到授权,但是访问是被禁止的
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作
  • 406 Not Acceptable - [GET]:用户请求的格式错误
  • 410 Gone -[GET]:用户请求的资源被永久删除
  • 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误

首先要正确的返回状态码,在处理返回的时候不要自定义返回码,比如说默认返回2XX的返回状态码将会给前端开发带来很多困扰。其次需要返回给用户错误的信息,一般来说,可以返回如下内容,程序将侦错的内容(error information发送给客户端,对业务类异常,用它指定的错误文本;对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”等,另外也可以将错误进行编码,然后返回时仅返回错误编码,前端开发可以在文档中查阅相关问题,这样在出错时可以节省网络占用:

1
2
3
{
error: "error information"
}

RESTful api的安全性

RESTful API将资源以及相应的合法操作暴露出来,这样开发者可以在合法操作下对资源进行处理。RESTful API在后端资源和前端应用之间起一个承接的作用,是系统暴露给外界的接口终端,所以,其安全性非常重要。安全并单单不意味着对资源进行加密解密,还包括一致性(integrity),机密性(confidentiality)和可用性(availibility)。

对请求数据的验证

对于客户端发起的请求进行验证,可以有效的过滤到带有攻击色彩的请求,对请求的内容进行严格的要求,一旦不符合规范要求即返回错误码,验证包含对Request的Headers,URI,Body的验证。

比如说在Headers中可以加入Oauth2.0的验证机制,或者有一个约定好的Request-ID,如果在Headers中没有找到相应的字段则报错,再比如在请求的Body或者URI添加了不该有的数据,所以在处理请求时需要对传入的数据进行限制,如果出现了不需要的数据则报错。

我们从数据流入REST API的第一步 —— 请求数据的验证 —— 来保证安全性。你可以把请求数据验证看成一个巨大的漏斗,把不必要的访问统统过滤在第一线:

对数据完整性的验证

REST API往往需要对后端的资源进行修改更新。这类操作需要很谨慎,我们既要保证正常的服务请求能够正确处理,还需要防止各种潜在的攻击。因此需要对请求中数据的完整性进行验证,需要至少保证要修改的数据和服务器里的数据是一致的。这个可以通过Etag来完成。

Etag可以认为是某个资源的一个唯一的版本号。当客户端请求某个资源时,该资源的Etag一同被返回,而当客户端需要修改该资源时,需要通过”If-Match”头来提供这个Etag。服务器检查客户端提供的Etag是否和服务器同一资源的Etag相同,如果相同,才进行修改,否则返回412 precondition failed。

使用Etag可以防止错误更新。比如A拿到了Resource X的Etag X1,B也拿到了Resource X的Etag X1。B对X做了修改,修改后系统生成的新的Etag是X2。这时A也想更新X,由于A持有旧的Etag,服务器拒绝更新,直至A重新获取了X后才能正常更新。

Etag类似一把锁,是数据完整性的最重要的一道保障。Etag能把绝大多数一致性的问题扼杀在摇篮中,当然,次序混乱还是存在的:如果B的修改还未进入数据库,而A的修改请求正好通过了Etag的验证时,依然存在一致性问题。这就需要在数据库写入时做一致性写入的前置检查。

对访问权限的验证

REST API需要清晰定义哪些操作能够公开访问,哪些操作需要授权访问。一般而言,如果对REST API的安全性要求比较高,那么,所有的API的所有操作均需得到授权。

在HTTP协议之上处理授权有很多方法,如HTTP BASIC Auth,OAuth,HMAC Auth等,其核心思想都是验证某个请求是由一个合法的请求者发起。Basic Auth会把用户的密码暴露在网络之中,并非最安全的解决方案,OAuth的核心部分与HMAC Auth差不多,只不过多了很多与token分发相关的内容。对于需要权限的请求,首先前置检查权限是否合格,如果权限不满足则返回4XX返回码,通过验证后的请求则正常进行处理。

成熟的RESTful框架

各个社区里面比较成熟的REST API framework/library:

  • JAVA: Spring-boot (spring)
  • Python: django-rest-framework(django),eve(flask)
  • Erlang/Elixir: webmachine/ewebmachine
  • Ruby: webmachine-ruby
  • Clojure:liberator

总结

REST风格的架构,是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。在前后端分离,单页面应用越来越流行的今天,通过书写RESTful风格的API可以很大程度上减少前后端工程师的开发工作量,按照RESTful的规则进行设计,可以保证接口的可用性,可扩展性,尽可能的应用安全策略可以保证接口在使用过程中不会对后端系统产生威胁。

就像REST之父Roy Fielding所说的,“(设计这个规范)就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。“。RESTful风格将在至少十年时间内继续在web开发上占据重要的地位。