前言
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)的情况,一般有几种处理方法。
-
使用POST,为需要的动作增加一个endpoint,使用POST来执行动作,比如:POST /resend重新发送邮件。
-
增加控制参数,添加动作相关的参数,通过修改参数来控制动作。比如一个博客网站,会有把写好的文章“发布”的功能,可以用上面的POST /articles/{:id}/publish方法,也可以在文章中共增加publish:boolen字段,发布的时候就是更新该字段PUT /articles/{:id}?published=true
-
把动作转换成资源,把动作转换成可以执行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、 跨域认证的问题
互联网服务离不开用户认证。一般流程是下面这样。
- 用户向服务器发送用户名和密码
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个session_id,写入用户的Cookie。
- 用户随后的每一次请求,都会通过Cookie,将session_id传回服务器。
- 服务器收到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的几个特点
- JWT默认是不加密,但也是可以加密的。生成原始Token以后,可以用密钥再加密一次。
- JWT不加密的情况下,不能将秘密数据写入JWT。
- JWT不仅可以用于认证,也可以用于交换信息。有效使用JWT,可以降低服务器查询数据库的次数。
- JWT的最大缺点是,由于服务器不保存session状态,因此无法在使用过程中废止某个token,或者更改token的权限。也就是说,一旦JWT签发了,在到期之间就会始终有效,除非服务器额外的逻辑。
- JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短,对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT不应该使用HTTP协议明码传输,要使用HTTPS协议传输。