RESTful API

"RESTful API"

Posted by Stephen on April 5, 2020

前言

RESTful API 一种流行的API设计风格

环境

系统环境

Distributor ID:	Ubuntu
Description:	Ubuntu 18.04.4 LTS
Release:	18.04
Codename:	bionic
Linux version :       5.3.0-46-generic ( buildd@lcy01-amd64-013 ) 
Gcc version:         7.5.0  ( Ubuntu 7.5.0-3ubuntu1~18.04 )

软/信息

version : 	
     None

正文

RESTful架构

REST是 Representational State Transfer的缩写(资源[Resources],表现层[Representation],状态转换[State Transfer]),如果一个架构符合REST原则,就称它为RESTful架构。

RESTful架构可以充分的利用HTTP协议的各种功能,是HTTP协议的最佳实践。

RESTful API是一个软件架构风格,设计风格,可以让软件更加清晰,更简洁,更有层次,可维护性更好

HTTP Methods path 备注 CRUD
GET /zoos 列出所有动物园 R
POST /zoos 新建一个动物园 C
GET /zoos/:id 获取某个指定动物园的信息 R
PUT /zoos/:id 更新某个指定动物园的全部信息 U
PATCH /zoos/:id 更新某个指定动物园的部分信息 U
DELETE /zoos/:id 删除某个动物园 D
GET /zoos/:id/animals 列出某个指定动物园的所有动物 R
DELET /zoos/:id/animals/:id 删除某个指定动物园的指定动物 D

请求 = 动词 + 宾语

动词使用五种HTTP方法,对应CRUD操作。

宾语URL应该全部使用名词复数,可以有例外,比如搜索可以使用更加直观的search。

过滤信息(Filtering)如果记录数量很多,API应该提供参数,过滤返回结果。?limit=10指定返回记录的数据?offset=10指定返回记录的开始位置。

使用HTTP的状态码

客户端的每一次请求,服务器都必须给出回应,回应包括HTTP状态码和数据两部分。

五大类状态码,总共100多种,覆盖了绝大部分可能遇到的情况。每一种状态码都有约定的解释,客户端只需查看状态码,就可以判断发生了什么情况,API不需要1xx状态码

状态码 状态描述
1xx 相关信息
2xx 操作成功
3xx 重定向
4xx 客户端错误
5xx 服务器错误

http常见的状态码,进阶了解更多信息,请查看维基百科-HTTP状态码

服务器回应数据

客户端请求时,要明确告诉服务器,接收json格式,请求的http头的ACCEPT属性要设成application/json

服务端返回的数据,不应该时纯文本,而应该是一个json对象。服务器回应的HTTP头的Content-Type属性要设为application/json

错误处理 如果状态码是4xx,就应该向用户返回出错信息,一般来说,返回的信息中将error作为键名,出错信息作为键值即可。{error:”Invalid API key”}

认证RESTful API 一共是无状态,每个请求应该带有一些认证凭证。推荐使用JWT认证,并且使用SSL

Hypermedia即返回结果中提高的链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

请求设计

1、动词

HTTP Methods 含义 CRUD
GET 读取 READ
POST 新建 Create
PUT 更新 Update
PATCH 更新 Update
DELETE 删除 Delete

2、宾语必须是名词

宾语就是API的URL,是HTTP动词作用的对象。它应该是名词,不能是动词。比如,/articles这个URL就是正确的,而下面的URL不是名称,所以都是错误的

/getAllCars
/createNewCar
/deleteAllRedCars

既然URL是名词,为了统一起见,建议都是使用复数。

3、设计参考

HTTP Methods path 备注
GET /zoos 列出所有动物园
POST /zoos 新建一个动物园
GET /zoos/:id 获取某个指定动物园的信息
PUT /zoos/:id 更新某个指定动物园的全部信息
PATCH /zoos/:id 更新某个指定动物园的部分信息
DELETE /zoos/:id 删除某个动物园
GET /zoos/:id/animals 列出某个指定动物园的所有动物
DELET /zoos/:id/animals/:id 删除某个指定动物园的指定动物

4、过滤信息(Filtering)

如果记录数量很多,服务器不能都将它们返回给用户。API应该提供参数,过滤返回结果。

下面是一些常用的参数。

?limit=100 # 指定返回记录的数量
?offset=10 # 指定返回记录的开始位置
?page=2&per_page=100 # 指定第几页,以及每页的记录数
?sortby=name&order=asc # 指定返回按照哪个属性排序,以及排序顺序
?animal_type_id=1	# 指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如。GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

5、不符合CRUD情况的RESTful API

在实际资源操作中,总会有一些不符合CRUD(Create-Read-Update-Delete)的情况,一般有几种处理方法。

  1. 使用POST,为需要的动作增加一个endpoint,使用POST来执行动作,比如:POST /resend重新发送邮件。

  2. 增加控制参数,添加动作相关的参数,通过修改参数来控制动作。比如一个博客网站,会有把写好的文章“发布”的功能,可以用上面的POST /articles/{:id}/publish方法,也可以在文章中共增加publish:boolen字段,发布的时候就是更新该字段PUT /articles/{:id}?published=true

  3. 把动作转换成资源,把动作转换成可以执行CRUD操作的资源,github就是用了这种方法。

    比如:喜欢一个gist,就增加一个/gist/:id/star 子资源,然后对其进行操作:”喜欢”使用PUT /gists/:id/star。“取消喜欢“使用 DELETE /gist/:id/star。

    另外一个例子是Fork。这也是一个动作,但是在gist下面增加forks资源,就是把动作变成CRUD兼容的 POST /gists/:id/forks 可以执行用户fork的动作。

返回设计

1、概叙

客户端的每一次请求,服务器都必须给出回应。回应包括HTTP状态码和数据两部分。

HTTP状态就是一个三位数,分成五个类别。

状态码 状态描述
1xx 相关信息
2xx 操作成功
3xx 重定向
4xx 客户端错误
5xx 服务器错误

这五类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况,每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

2、状态码

1xx状态码

API不需要1xx状态码,下面介绍其他四类状态码的精确含义。

2xx状态码

200状态码表示操作成功,但是不同的方法可以返回更精确的状态码。

HTTP Methods HTTP code 含义
GET 200 OK
POST 201 Created
PUT 200 OK
PATCH 200 OK
DELETE 204 No Content

上面代码中,POST返回201状态码,表示生成了新的资源;DELETE返回204状态码,表示资源已经不存在。

3xx状态码

API用不到301状态码(永久重定向)和302状态码(暂时重定向,307也是这个含义),因为他们可以由应用级别返回,浏览器会直接跳转,API级别额可以不考虑这两种情况。

API主要是用303 See Other,表示参考另外URL。它与302和307的含义一样,也是暂时重定向,区别在于302和307用于GET请求,而303用于POST、PUT和DELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。

HTTP/1.1 303 See other
Location: /api/orders/123456
4xx状态码

4xx状态码表示客户端错误,主要有下面几种:

状态码 含义
400 Bad Request 服务器不理解客户端的情况,未做任何处理
401 Unauthorized 用户未提供身份证凭据,或者没有通过身份验证
403 Forbidden 用户通过了身份验证,但是不具有返回资源所需的权限
404 Not Found 所请求的资源不存在,或不可用
405 Method Not Allowed 用户已经通过身份验证,但是所用的HTTP方法不在它的权限之内。
410 Gone 所请求的资源已从这个地址转移,不再可用
415 Unsupported Media Type 客户端要求返回格式不支持,比如API只能返回json格式,但是客户端要求返回xml格式。
422 Unprocessable Entity 客户端上传的附件无法处理,导致请求失败
429 Too Many Requests 客户端的请求次数超过限额
5xx状态码

5xx状态码表示服务端错误,一般来说,API不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

状态码 含义
500 Internal Server Error 客户端请求有效,服务器处理时发生了意外
503 Service Unavailable 服务器无法处理请求,一般用于网站维护状态

3、返回数据

3.1 不要返回纯文本

API返回的数据格式,不应该是纯文本,而应该是一个json对象,因为这样才能返回标准的结构化数据。所以,服务器回应的HTTP头的Content-Type属性要设为application/json.

客户端请求时,也要明确告诉服务器,可以接受JSON格式,即请求的HTTP头的ACCEPT属性也要设成application/json。

3.2 不要包装数据

response的body直接就是数据,不要做多余的包装。错误实例:

{"success":true,"data":{"id":1,"name":"杨过"}}

针对不同操作,服务器向用户返回的结果应该符合以下规范。

HTTP Methods URL 含义
GET /collection 返回资源对象的列表
GET /collection/resource 返回单个资源对象
POST /collection 返回新生成的资源对象
PUT /collection/resource 返回完整的资源对象
PATCH /collection/resource 返回完整的资源对象
DELETE /collection/resource 返回一个空文档
3.3发生错误时,不要返回200状态码

有一种不恰当的做法:即使发生错误,也返回200状态码,把错误信息放在数据体里面,就像下面这样。

{"status":"failure","data":{"error":"Expected at least two items in list."}}

正确做法是,状态码发生的错误,具体的错误信息放在数据体里面返回。

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
	"error":"Invalid payload",
	"detail":{
		"surname":"This field is required."
	}
}

JWT认证

Json Web Token(缩写JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。

1、 跨域认证的问题

互联网服务离不开用户认证。一般流程是下面这样。

  1. 用户向服务器发送用户名和密码
  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
  3. 服务器向用户返回一个session_id,写入用户的Cookie。
  4. 用户随后的每一次请求,都会通过Cookie,将session_id传回服务器。
  5. 服务器收到session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好,单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求session数据共享,每台服务器都能读取session。

举例来说,A网站和B网站是同一家公司的关联服务。现在要求,用户只要在其中一个网址登录,再访问另外一个网站会自动登录,请问怎么实现?

一种解决方案是session数据持久化,写入数据库或者别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另外一种方案是服务器索性不保存session数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT就是这种方案的一个代表。

2、JWT的原理

JWT的原理是,服务器认证以后,生成一个JSON对象,发回给用户,就像下面这样。

{
	"姓名":"张三",
	"角色":"管理员",
	"到期时间":"2018/1/1",
}

以后,用户与服务端通信的时候,都要发回这个JSON对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(见后文)。

服务器就不保存任何session数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

3、JWT的数据结构

实际的JWT大概就像下面这样。

asdjflkajsdflkaasdfjlakssafd.asdjflakjsdflkajsdf2jasldkfj.alskdfjlaksjdflkjasdflk

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT内部是没有换行的。

JWT的三部分依次如下:Header.Payload.Signature

3.1 Header

Header部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。

{
	'alg':'HS256',
	'typ':'JWT',
}

上面代码中,alg属性表示签名的算法(algorithm),默认是HMAC SHA256(写成HS256);typ属性表示这个令牌(token)的类型(type),JWT令牌统一写为JWT。

最后,将上面的JSON对象使用Base64URL算法(详见后文)转成字符串。

3.2 Payload

Payload部分也是一个JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段,供选用。

iss(issusr):签发人
exp(expiration time): 过期时间
sub(subject): 主题
aud(audience):受众
nbf( Not Before):生效时间
iat(issued At):签发时间
jti(JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个雷子

{
	"sub":"12314564869",
	"name":'test',
	"admin":true
}

注意,JWT默认是不加密,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个JSON对象也要使用Base64URL算法转成字符串。

3.3 Signature

Signature部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户,然后,使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(base64UrlEncode(header)+ “.” + base64UrlEncode(payload), secret)

算出签名以后,把Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用点(.)分隔,就返回给用户。

3.4 Base64URL

前面提到,Header和Payload串型化的算法Base64URL。这个算法跟Base64算法基本类似,但有些小的不同。

JWT作为一个令牌(token),有些场合可能会放到URL(比如api.example.com/?token=xxx)。Base64 有三个字符+、/、= ,在URL里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_,这就是Base64URL算法。

4、JWT的使用方式

客户端收到服务器返回的JWT,可以储存在Cookie里面,也可以储存在localStorage。

此后,客户端每次与服务器通信,都要带上这个JWT。你可以把它放到Cookie里面自动发送,但是这个不能跨域,所以更好的做法是放在HTTP请求头信息Authorization字段里面。

Authorization: Bearer

另一种做法是,跨域的时候,JWT就放在POST请求的数据体里面。

5、JWT的几个特点
  1. JWT默认是不加密,但也是可以加密的。生成原始Token以后,可以用密钥再加密一次。
  2. JWT不加密的情况下,不能将秘密数据写入JWT。
  3. JWT不仅可以用于认证,也可以用于交换信息。有效使用JWT,可以降低服务器查询数据库的次数。
  4. JWT的最大缺点是,由于服务器不保存session状态,因此无法在使用过程中废止某个token,或者更改token的权限。也就是说,一旦JWT签发了,在到期之间就会始终有效,除非服务器额外的逻辑。
  5. JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短,对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT不应该使用HTTP协议明码传输,要使用HTTPS协议传输。