HTTP 与 RESTful API 设计
0x00 准备
-
开发工具:VSCode(或 Postman 等)
-
VSCode 插件:REST Client
-
文件扩展名:.rest(或 .http)
-
测试:
-
在 design.rest 中写入以下代码:
GET https://cataas.com/cat
-
点击代码上方出现的
Send Request
-
默认情况下,在右侧会打开 REST Client 的响应窗口,完成测试
-
0x01 概述
(1)分布式 API 的历史
- 1970s~1980s,分布式计算初期:RPC、消息队列
- 1980s~1990s,面向对象 API 时期:COM/DCOM、CORBA、Java RMI
- 2000s~今,基于 Web 的 API 时期:XMLHTTP、REST、SOAP、GraphQL、gRPC
(2)HTTP 工作原理
-
客户端(如个人电脑)向服务端(如云服务器)发送请求(Request),请求包括:
- 请求方法(verb):希望服务端执行的操作,常见方法包括:
- GET:获取资源
- POST:创造资源
- PUT:更新资源
- PATCH:更新部分资源
- DELETE:删除资源
- 请求头(headers):关于请求的信息,请求的元数据,常见请求头包括:
- Content-Type:请求内容的类型
- Content-Length:请求内容的长度
- Authorization:请求者身份认证
- Accept:允许接受的类型
- Cookies:请求中的客户端数据
- 请求体(content):请求的内容,发送到服务端的数据,常见请求体包括:
- HTML、CSS、JavaScript、XML、JSON 等
- 内容对某些方法无效
- 帮助满足请求的信息
- 二进制信息
举例:
POST Content-Length: 11 Hello World
- 其中,
POST
是请求方法;Content-Length: 11
是请求头;Hello World
是请求内容
- 请求方法(verb):希望服务端执行的操作,常见方法包括:
-
服务端接收请求并处理后,向客户端发送响应(Response),响应包括:
-
状态码(status code):请求的执行状态,码类如下:
- 100-199:一般信息(Informational)
- 200-299:成功信息(Success)
- 300-399:重定向信息(Redirection)
- 400-499:客户端错误(Client Errors)
- 500-599:服务端错误(Server Errors)
详细说明参考 HTTP 常见状态码说明 | SRIGT
-
响应头(headers):关于响应的信息,响应的元数据,常见响应头包括:
- Content-Type:响应内容的类型
- Content-Length:响应内容的长度
- Expires:响应有效期
- Cookies:请求携带的数据
-
响应体(content):响应的内容,常见响应内容包括:
- HTML、CSS、JavaScript、XML、JSON 等
- 二进制信息
举例:
201 Contet-Type: text Hello World
-
-
使用 REST Client 收发 HTTP 请求与响应
-
发送基本 GET 请求
GET https://restdesign.dev
GET https://restdesign.dev/css/site.css
-
将 URL 地址声明成变量并使用
@site = https://restdesign.dev GET {{site}} ### GET {{site}}/api/customers Accept: application/json
###
用于在同一文件中区别不同的 HTTP 请求
-
(3)REST
- REST(REpresentational State Transfer)是指状态转移,概念包括:
- 客户端与服务端分离
- 服务器是无状态的
- 请求是可缓存的
- 使用统一的接口
- 存在一些问题:
- 难以被认定为完全符合 REST 标准
- 被认为教条而非实用
- 结构化构建风格
- 需要富有成效
- 精心设计的 API 的参考:https://api.github.com
0x02 设计 RESTful API
(1)设计 API
-
无法在发布 API 后对其设计进行修复,因此需要首先完成 API 的设计,好处包括:
- 很容易添加临时端点
- 帮助理解请求
-
REST APIs 有以下几个部分:
- 请求
- 请求方法
- URI,统一资源标识符(Uniform Resource Identifier),包括查询字符串(Query String)参数
- 请求头
- 请求体,根据情况可选
- 响应
- 状态码
- 响应头
- 响应体
- 请求
-
URI
-
是资源的路径,其中包括非数据元素的查询字符串
-
是资源的唯一标识符,但不必是主键
-
举例:
/user /user/1 /user/SR1GT
-
-
查询字符串
-
使用非资源属性
-
举例:
/user?number=1 /user?name=SR1GT
-
(2)请求方法
-
GET:获取
GET https://restdesign.dev/api/customers/1 Accept: application/json
-
POST:创造资源
POST https://restdesign.dev/api/customers/ Accept: application/json Content-Type: application/json { "companyName": "XXX Inc", "contact": null, "phoneNumber": "123-456-7890", "email": null, "addressLine1": "221B Baker Street", "addressLine2": null, "addressLine3": null, "city": null, "stateProvince": null, "postalCode": "12345-6789", "country": null, "projects": [] }
-
PUT:更新资源
PUT https://restdesign.dev/api/customers/26 Accept: application/json Content-Type: application/json { "companyName": "YYY Inc", "contact": null, "phoneNumber": "098-765-4321", "email": null, "addressLine1": "221B Baker Street", "addressLine2": null, "addressLine3": null, "city": null, "stateProvince": null, "postalCode": "98765-4321", "country": null, "projects": [] }
- 其中,URI 中的
26
是来自 POST 请求的响应
- 其中,URI 中的
-
DELETE:删除资源
DELETE https://restdesign.dev/api/customers/26 Accept: application/json
(3)幂等
- 幂等(Idempotent)是指可以多次应用而不更改结果的操作
- REST 中存在幂等
- 操作产生相同的结果和副作用
- 如:每次 GET 的结果相同,除非数据状态发送改变
- POST 方法不是幂等
- 操作产生相同的结果和副作用
(4)设计成果
-
设计成果中的成员名称:
- 不应暴露服务器细节
- 建议使用驼峰命名法
-
设计过程中需要决定格式
-
遵守
Accept
的规则Accept: application/json,text/xml
-
举例:
GET https://restdesign.dev/api/projects Accept: text/xml
-
-
返回相同格式的数据,一般是 JSON
Content-Type: application/json
-
最好不要通过查询字符串控制格式
/api/user?format=json
-
常见格式:
- JSON:application/json
- XML:text/xml
- JSONP:application/javascript*
- RSS:application/xml+rss
- ATOM:application/xml+atom
-
0x03 复杂的 API 场景
(1)关联
-
对子对象使用 URI 导航,如:
/api/user/1/name /api/product/1/name
-
返回相同形状的列表
/api/user/1/username /api/username
-
单个端点可以有多个关联
/api/user/1/name /api/user/1/age
-
使用多个查询搜索
/api/user?age=18&st=NY
-
举例:
GET https://restdesign.dev/api/projects/1/tickets Accept: application/json ### GET https://restdesign.dev/api/tickets Accept: application/json
(2)分页
-
列表应支持分页
-
使用查询字符串进行分页,如:
GET https://restdesign.dev/api/tickets?page=1&page_size=25 Accept: application/json
-
使用包装标识分页
{ "totalCount": 100, "prevPage": "/api/tickets?page=1&pageSize=10", "nextPage": "/api/tickets?page=3&pageSize=10", "currentPage": 2, "pageSize": 10, "results": [...] }
-
也可以使用响应头来控制分页
GET https://restdesign.dev/api/tickets?useHeaders=true Accept: application/json
X-Pagination-TotalCount: 100 X-Pagination-PreviousPage: X-Pagination-NextPage: /api/tickets?page=2&pageSize=10 X-Pagination-CurrentPage: 1 X-Pagination-PageSize: 10
(3)错误处理
-
不止状态码预设的错误,可以返回包含错误信息的对象
{ error: "Invalid value" }
- 但是明显的错误没必要自定义错误信息,使用状态码即可,如 404 Not Found
-
传达错误
-
帮助用户从错误中恢复
-
举例:
POST https://restdesign.dev/api/customers Accept: application/json Content-Type: application/json { "companyName": "" }
响应内容:
HTTP/1.1 400 Bad Request Content-Type: application/problem+json { "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "CompanyName": [ "'Company Name' must not be empty.", "The length of 'Company Name' must be at least 5 characters. You entered 0 characters." ] } }
(4)缓存
-
缓存是 REST APIs 的基本原则
-
此处的缓存主要指客户端的缓存
-
使用 HTTP 进行缓存机制
-
客户端发起对指定版本的 GET 请求
GET URI Version: last_xyz Hello World
-
服务端返回响应
304 Not Modified
-
客户端发起 PUT 请求
PUT URI If-Match: last_xyz Hello World
-
如果
If-Match
的版本匹配失败,则服务端返回 412 响应412 Precondition Failed
-
-
使用 ETag(Entity Tag,实体标签)解决上述 HTTP 缓存机制关于版本的问题
- 支持强缓存和弱缓存
- 在响应头中返回(前缀带
W\
表示弱缓存) - 请求头可携带
If-None-Match: ETag
- GET 请求使用 304 状态码表明已缓存
- PUT/DELETE 请求使用 412 状态码表明 ETag 不符
(5)函数式 API
-
函数式 API 追求实用,用于执行操作而非修改数据
-
函数式 API 并非是构建 RPC API 的理由
-
举例:
/api/calculateTax/state=GA&total=149.99 /api/restartServer/?isColdBoot=true
(6)异步 API
- 某些 API 本质上并非 REST 风格
- 需要持久、轮询
- 非 REST 方案是有用的
- Comet:持久与轮询
- gRPC:支持双向和单向通信信号
- Firebase
- Socket.IO
0x04 API 版本控制
(1)版本控制必要性
- 对 API 版本控制的思考
- 对于已发布的 API,它是无法被修改的
- 用户依赖于不变的 API
- 但需求可能会变化
- 在不破坏客户端的情况下演进 API
- API 版本与产品版本相互独立
- 版本控制要求:
- 需要同时兼容新旧版本
- 并行开发是不可行的
(2)版本控制策略
a. 基于 URI
- 基于 URI
- 如:/api/v2/user
- 优点:可以清楚正在使用的版本
- 缺点:每个版本需要对 URI 进行修改调整,降低 API 复用性
- 基于查询字符串
- 如:/api/user?v=2.0
- 优点:版本在 URI 中可选填(即,可使用默认版本)
- 缺点:客户端很容易忽视需要的版本
b. 基于请求头
-
基于请求头
- 如:
X-Version: 2.0
- 优点:将版本控制从 REST API 中分离
- 缺点:需要更多经验丰富的开发者调整请求头
- 如:
-
基于请求头的
Accept
- 如:
Accept: application/json;version=2.0
- 优点:无需创建自定义头信息
- 缺点:比查询字符串更不明显(容易被忽视)
- 如:
-
基于请求头的
Content-Type
-
如:
GET /api/user HTTP/1.1 Content-Type: application/example.user.v2+json Accept: application/example.user.v2+json
-
优点:可以版本有效负载以及API调用本身
-
缺点:需要更多的开发周期来创建和维护
-
0x05 保护 API
(1)API 与 安全
- 以下情况需要 API 安全
- 使用私有或专有数据
- 发送敏感数据
- 使用任何凭据,如密码
- 考虑防止服务器免受滥用
- API 安全面临的威胁
- 传输窃听
- 如:软件 Packet Sniffers
- 服务端的侵入与物理安全
- 来自客户端的黑客攻击
- 传输窃听
- API 防护方向
- 服务器基础结构安全性
- 超出 API 安全范围
- 传输安全
- SSL(Security Socket Layer),安全套接层,一种网络安全协议
- 保护 API 本身
- 跨域安全
- 身份验证与授权
- 服务器基础结构安全性
(2)跨域安全
-
在浏览器默认提供跨域安全保护,这也意味着 Python 脚本之类的默认可以跨域
-
对于私有 API 需要着重考虑跨域安全,而公开 API 则应允许跨域
-
通过 CORS(Cross-Origin Resource Sharing,跨源资源共享)允许用户跨域使用 API,CORS 流程如下:
-
客户端发起跨源请求
-
浏览器请求访问
OPTIONS /api/user HTTP/1.1 Origin: http://example.com Access-Control-Request-Method: POST Host: localhost:8080
Origin
:目标源Access-Control-Request-Method
:请求访问的方法Host
:来源
-
服务端回复规则
Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Origin: http://example.com Content-Type: 0
-
浏览器发出 CORS 请求头
POST /api/user HTTP/1.1 Origin: http://example.com Access-Control-Request-Method: POST Host: localhost:8080 Content-Type: 0
-
(3)认证与授权
-
认证与授权的对比
认证 Authentication 授权 Authorization 用户身份 操作项 凭据
如令牌(Token)、账密、第三方证书等权限
如声明、角色、权力 -
常见身份认证类型
- 应用认证,如 AppID+密钥
- 用户认证,如 Basic Auth、Cookies、Token Auth、OAuth2.0(Open Authorization)
(4)认证方法
a. Basic Auth
- 容易实现
- 不安全,除非强制执行 SSL
- 在每次的请求中都发送凭据
- 增加了(网络黑客)攻击区域面
b. Cookies
- 很容易使用,也很常见
- 由于存储在浏览器中,容易被窃取并伪造
- 是否选择使用 Cookies 保护 API,需要根据安全需求
c. Token
-
令牌(Token)是一个字符串,客户端无需对令牌解密
- 令牌认证是典型的 API
- 通常令牌的有效期比 Cookies 更短,为 5 到 20 分钟
-
令牌认证流程:
sequenceDiagram participant Client participant Server Client->>Server: 发送凭据 Server->>Client: 返回令牌 Client->>Server: 在随后的调用中包含令牌 Server->>Client: 验证令牌 -
JWTs(JSON Web Tokens)是最常见的令牌类型
-
行业标准
-
独立、体积小、完整
-
一般包括以下内容:
- 用户信息
- 权限声明
- 验证签名
- 其他信息
-
结构如下:
各部分在令牌中以句点
.
结束-
标头:令牌的元数据,如
{ "alg": "HS256" }
-
载荷:令牌中的数据,如:
{ "Username": "SR1GT", "exp": 1710000000000 }
-
签名:用于验证令牌,如:
HMACSHA256( base64UrlEncode(header) + '.' + base64UrlEncode(payload), pluralsight-jwt )
-
-
d. OAuth
-
使用可信第三方验证
-
服务端不会收到凭据
- 用户通过第三方验证,使用令牌确认凭据
-
工作流程:
- 开发者请求 API 密钥
- API 支持 API 密钥并分享密码
- 开发者请求一个令牌
- API 验证并返回令牌
- 开发者重定向到 API 的认证 URI
- API 显示授权 UI
- 用户确认授权
- API 重定向回到开发者
- 开发者通过 OAuth 请求访问令牌并请求令牌
- API 返回带有超时限制的访问令牌
- 开发者可以使用带有访问令牌的 API 知道超时
-End-