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:soap
、xmlns:tem
、xmlns: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。
跨域问题
互联网无法离开用户认证,用户认证的流程一般如下:
- 用户输入用户名和密码,提交到服务器
- 服务器验证用户名和密码,如果正确,在当前对话(session)中记录用户的登录状态
- 服务器返回一个包含用户信息(session_id)的 cookie 给浏览器
- 浏览器将 cookie 保存起来,以后每次请求都会带上这个 cookie,将这个 seesion_id 发送给服务器
- 服务器验证 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
它是一个很长的字符串,由三部分组成,中间用点分隔开,分别是:Header
、Payload
、Signature
。
Header
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
)
header
和 payload
都是经过 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 协议传输。