56

RESTFUL API 介绍

RESTFUL API 是一种基于 HTTP 协议的 API 设计风格。

RESTFUL API 介绍

为什么要使用RESTful API

REST 是 Representational State Transfer 的缩写,中文翻译为“表现层状态转化”。REST 是一种软件架构风格,它提供了一组设计原则和约束条件,用于创建 Web 服务。RESTful API 是一种基于 REST 架构风格的 API,它的设计遵循 REST 的设计原则和约束条件。

它的架构风格、设计风格可以让软件更加清晰、更加易于理解、更加易于维护。

RESTful API 的特点

  • RESTful API 是基于 HTTP 协议的,所以它的特点就是 HTTP 协议的特点。
  • RESTful API 是无状态的,这意味着服务器不会保存客户端的状态,每次请求都是独立的,服务器不会记住上一次请求的信息。
  • RESTful API 是可缓存的,这意味着服务器可以缓存客户端的请求,下次请求时直接返回缓存的结果,而不是重新计算。
  • RESTful API 是可扩展的,这意味着服务器可以根据客户端的需求,返回不同的数据格式,比如 JSON、XML、YAML 等。

RESTful API 的诞生

RESTful API 的诞生是为了解决 Web 服务的一些问题,比如:

  • 传统的 Web 服务是基于 SOAP 协议的,而 SOAP 协议的数据格式是 XML,这样就会导致数据冗余,比如:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tem="http://tempuri.org" xmlns:ns="http://schemas.datacontract.org/2004/07/MyService">
   <soap:Header/>
   <soap:Body>
      <tem:GetUser>
         <tem:userId>1</tem:userId>
      </tem:GetUser>
   </soap:Body>
</soap:Envelope>

上述 XML 数据中,有很多重复的信息,比如:xmlns:soapxmlns:temxmlns:ns 这些都是命名空间,它们的作用是为了区分不同的数据,但是这些数据对于客户端来说是没有用的,所以就会导致数据冗余。

设计示例

请求方法请求路径作用
GET/zoos列出所有动物园
POST/zoos新增一个动物园
GET/zoos/:id获取某个指定动物园的信息
PUT/zoos/:id更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH/zoos/:id更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE/zoos/:id删除某个动物园
GET/zoos/:id/animals列出某个指定动物园的所有动物
DELETE/zoos/:id/animals/:animal_id删除某个指定动物园的指定动物

也就是 动词 + 宾语 的设计风格。

HTTP 动词

HTTP 动词是用来描述 API 请求的动作的,常见的有 GET、POST、PUT、DELETE 等,这些动词都是符合 RESTful API 设计的。

  • GET:读取(Read)
  • POST:新建(Create)
  • PUT:更新(Update)
  • PATCH:更新(Update),通常用于部分更新
  • DELETE:删除(Delete)

URL 名词

宾语就是 API 中的 URL 名词,它是用来描述 API 请求的对象的,常见的有 users、posts、comments 等,这些名词都是符合 RESTful API 设计的。

/users // ✔ 
/posts // ✔
/articles // ✔
/getAllCars // ✖️
/getAllUsers // ✖️

案例

请求方法请求路径作用
GET/zoos列出所有动物园
POST/zoos新增一个动物园
GET/zoos/:id获取某个指定动物园的信息
PUT/zoos/:id更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH/zoos/:id更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE/zoos/:id删除某个动物园
GET/zoos/:id/animals列出某个指定动物园的所有动物
DELETE/zoos/:id/animals/:animal_id删除某个指定动物园的指定动物

过滤信息

如果数据过多,不能简单地将所有数据都返回给用户,这时就需要过滤信息。过滤信息的方法是在 URL 后面加上查询字符串(query string),即 URL 中加上 ?key=value 的形式,多个查询条件之间用 & 分隔。

GET /zoos?limit=10&offset=20    // 获取前 10 个动物园且跳过前 20 个
GET /zoos?sortby=name&order=asc // 按名称升序排列
GET /zoos?fields=name,area      // 只返回名称和面积两个字段
GET /zoos?name=Beijing          // 只返回北京动物园的信息

参数的设计存在冗余,比如上面的例子中,如果要获取北京动物园的信息,可以直接写成 /zoos/Beijing,而不是通过 name 参数。

特殊情况

CURD (Create、Update、Retrieve、Delete)是最常见的操作,但是 RESTful API 并不限于此,还有一些特殊的操作,比如登录、登出、上传文件等。一般来说,这些操作的 URL 都是固定的,不会随着资源的变化而变化,所以这些操作的 URL 通常都是动词,而不是名词。

POST /login    // 登录
POST /logout   // 登出
POST /upload   // 上传文件

或者某些接口中有特殊的功能,如 /articles/:id/publish

动词覆盖

有些客户端只能使用 GET 和 POST 两种方法,服务器必须接收用 POST 来模拟其他三个方法。这时,通过 HTTP 发出去的 POST 请求,服务器会根据请求头中的 X-HTTP-Method-Override 字段来判断请求的真实方法。

POST /zoos/1 HTTP/1.1
X-HTTP-Method-Override: DELETE

API 请求

HTTP 动词

HTTP 动词是用来描述 API 请求的动作的,常见的有 GET、POST、PUT、DELETE 等,这些动词都是符合 RESTful API 设计的。

  • GET:读取(Read)
  • POST:新建(Create)
  • PUT:更新(Update)
  • PATCH:更新(Update),通常用于部分更新
  • DELETE:删除(Delete)

URL 名词

宾语就是 API 中的 URL 名词,它是用来描述 API 请求的对象的,常见的有 users、posts、comments 等,这些名词都是符合 RESTful API 设计的。

/users // ✔ 
/posts // ✔
/articles // ✔
/getAllCars // ✖️
/getAllUsers // ✖️

案例

请求方法请求路径作用
GET/zoos列出所有动物园
POST/zoos新增一个动物园
GET/zoos/:id获取某个指定动物园的信息
PUT/zoos/:id更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH/zoos/:id更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE/zoos/:id删除某个动物园
GET/zoos/:id/animals列出某个指定动物园的所有动物
DELETE/zoos/:id/animals/:animal_id删除某个指定动物园的指定动物

过滤信息

如果数据过多,不能简单地将所有数据都返回给用户,这时就需要过滤信息。过滤信息的方法是在 URL 后面加上查询字符串(query string),即 URL 中加上 ?key=value 的形式,多个查询条件之间用 & 分隔。

GET /zoos?limit=10&offset=20    // 获取前 10 个动物园且跳过前 20 个
GET /zoos?sortby=name&order=asc // 按名称升序排列
GET /zoos?fields=name,area      // 只返回名称和面积两个字段
GET /zoos?name=Beijing          // 只返回北京动物园的信息

参数的设计存在冗余,比如上面的例子中,如果要获取北京动物园的信息,可以直接写成 /zoos/Beijing,而不是通过 name 参数。

特殊情况

CURD (Create、Update、Retrieve、Delete)是最常见的操作,但是 RESTful API 并不限于此,还有一些特殊的操作,比如登录、登出、上传文件等。一般来说,这些操作的 URL 都是固定的,不会随着资源的变化而变化,所以这些操作的 URL 通常都是动词,而不是名词。

POST /login    // 登录
POST /logout   // 登出
POST /upload   // 上传文件

或者某些接口中有特殊的功能,如 /articles/:id/publish

动词覆盖

有些客户端只能使用 GET 和 POST 两种方法,服务器必须接收用 POST 来模拟其他三个方法。这时,通过 HTTP 发出去的 POST 请求,服务器会根据请求头中的 X-HTTP-Method-Override 字段来判断请求的真实方法。

POST /zoos/1 HTTP/1.1
X-HTTP-Method-Override: DELETE

API 响应

HTTP 动词

HTTP 动词是用来描述 API 请求的动作的,常见的有 GET、POST、PUT、DELETE 等,这些动词都是符合 RESTful API 设计的。

  • GET:读取(Read)
  • POST:新建(Create)
  • PUT:更新(Update)
  • PATCH:更新(Update),通常用于部分更新
  • DELETE:删除(Delete)

URL 名词

宾语就是 API 中的 URL 名词,它是用来描述 API 请求的对象的,常见的有 users、posts、comments 等,这些名词都是符合 RESTful API 设计的。

/users // ✔ 
/posts // ✔
/articles // ✔
/getAllCars // ✖️
/getAllUsers // ✖️

案例

请求方法请求路径作用
GET/zoos列出所有动物园
POST/zoos新增一个动物园
GET/zoos/:id获取某个指定动物园的信息
PUT/zoos/:id更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH/zoos/:id更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE/zoos/:id删除某个动物园
GET/zoos/:id/animals列出某个指定动物园的所有动物
DELETE/zoos/:id/animals/:animal_id删除某个指定动物园的指定动物

过滤信息

如果数据过多,不能简单地将所有数据都返回给用户,这时就需要过滤信息。过滤信息的方法是在 URL 后面加上查询字符串(query string),即 URL 中加上 ?key=value 的形式,多个查询条件之间用 & 分隔。

GET /zoos?limit=10&offset=20    // 获取前 10 个动物园且跳过前 20 个
GET /zoos?sortby=name&order=asc // 按名称升序排列
GET /zoos?fields=name,area      // 只返回名称和面积两个字段
GET /zoos?name=Beijing          // 只返回北京动物园的信息

参数的设计存在冗余,比如上面的例子中,如果要获取北京动物园的信息,可以直接写成 /zoos/Beijing,而不是通过 name 参数。

特殊情况

CURD (Create、Update、Retrieve、Delete)是最常见的操作,但是 RESTful API 并不限于此,还有一些特殊的操作,比如登录、登出、上传文件等。一般来说,这些操作的 URL 都是固定的,不会随着资源的变化而变化,所以这些操作的 URL 通常都是动词,而不是名词。

POST /login    // 登录
POST /logout   // 登出
POST /upload   // 上传文件

或者某些接口中有特殊的功能,如 /articles/:id/publish

动词覆盖

有些客户端只能使用 GET 和 POST 两种方法,服务器必须接收用 POST 来模拟其他三个方法。这时,通过 HTTP 发出去的 POST 请求,服务器会根据请求头中的 X-HTTP-Method-Override 字段来判断请求的真实方法。

POST /zoos/1 HTTP/1.1
X-HTTP-Method-Override: DELETE

JWT 认证

JSON Web Token 是目前最流行的跨域认证解决方案。JWT 也就是我们常用的 token。

跨域问题

互联网无法离开用户认证,用户认证的流程一般如下:

  1. 用户输入用户名和密码,提交到服务器
  2. 服务器验证用户名和密码,如果正确,在当前对话(session)中记录用户的登录状态
  3. 服务器返回一个包含用户信息(session_id)的 cookie 给浏览器
  4. 浏览器将 cookie 保存起来,以后每次请求都会带上这个 cookie,将这个 seesion_id 发送给服务器
  5. 服务器验证 seesion_id,如果正确,就返回数据给浏览器

这种认证方式存在一个问题,就是扩展性不好。单个服务器自然没有问题,但是如果有多个服务器,就会出现问题。它要求 session 数据能够在服务器之间共享,这样才能保证用户在访问不同服务器的时候,能够保持登录状态。

举例,A 网站和 B网站是同一家公司的关联服务,现在要求用户只要在 A 网站登录了,就能在 B 网站自动登录。这就需要 session 数据能够在 A 网站和 B 网站之间共享。

一种解决方案是将 session 持久化,将 session 数据存储到数据库中,这样就能够在不同服务器之间共享了。但是这样会带来一个问题,就是服务器的负载会变重,因为每次请求都需要访问数据库。

另一种解决方案就是服务器不再保存 session 数据,将所有的数据都保存在客户端,每次请求都发回到服务器,服务器只需要验证数据的有效性即可。这种方式就是 JWT 认证。

JWT 原理

服务器认证后,生成一个 JSON 对象,发回给用户,如下:

{
    "name": "Cheng",
    "admin": true
    "data": "2022-11-14",
}

此后,用户每次请求都会带上这个 JSON 对象,服务器只需要验证这个 JSON 对象的有效性即可。为了防止用户篡改数据,服务器会在生成 JSON 对象的时候,对其进行签名,如下:

{
    "name": "Cheng",
    "admin": true
    "data": "2022-11-14",
    "signature": "xxxxx"
}

实际的 JWT 大概像这样:

eydcjdnod9w7djwn82bwdkddko.
eyJkosdeLw0eo2hdbjJIu7RFCG08wd8d.
4psxcdfvekorw9d8f7g6h5j4k3l2m1n0

它是一个很长的字符串,由三部分组成,中间用点分隔开,分别是:HeaderPayloadSignature

2

Header 部分是一个 JSON 对象,用来描述 JWT 的元数据,如下:

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

alg 表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT。然后使用 Base64 编码,得到一个字符串。

Payload

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

  • iss:JWT 的签发者
  • sub:JWT 所面向的用户
  • exp:JWT 的过期时间,这个过期时间必须要大于签发时间
  • aud:接收 JWT 的一方
  • nbf:定义在什么时间之前,该 JWT 都是不可用的(生效时间)
  • iat:JWT 的签发时间
  • jti:JWT 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击

除了官方字段,你还可以在这个部分定义私有字段,如下:

{
    "name": "Cheng",
    "admin": true
    "data": "2022-11-14",
}

JWT 默认是不加密的,任何人都可以读到这个部分的数据,所以不要把敏感信息放在这个部分。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。它由三部分组成,如下:

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

headerpayload 都是经过 Base64 编码的字符串,然后用点连接起来,再通过密钥 secret 进行 HMAC SHA256 运算,最后得到的就是 Signature。

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

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

JWT 的使用

客户端收到 JWT,可以存储到 Cookie 或 Local Storage 中。

此后,客户端每次向服务器发送请求,都要带上这个 JWT。服务器收到请求,然后验证 JWT 的签名,如果通过,就知道这个请求是可信的。在 HTTP 请求中,JWT 通常被放在 Authorization 头信息中,如下:

Authorization: Bearer <token>

JWT 几个特点

  • JWT 默认是不加密的,任何人都可以读到里面的数据,所以不要把敏感信息放在 JWT 中。但是 JWT 是可以加密的。
  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此在使用 JWT 时,每次用户请求都必须携带 JWT,这会轻微地降低性能。无法在使用过程中废止某个 token,或者更改 token 的权限。如果需要这些功能,就只能不使用 JWT,使用数据库来保存 session 状态。
  • JWT 本身包含认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减轻这种危险,JWT 的有效期应该设置得比较短。另外,一旦发现令牌泄露,应该立即废止该令牌。
  • JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

各种语言的 JWT 实现