HTTP 与 RESTful API 设计

0x00 准备

  • 开发工具:VSCode(或 Postman 等)

  • VSCode 插件:REST Client

  • 文件扩展名:.rest(或 .http)

  • 测试:

    1. 在 design.rest 中写入以下代码:

      GET https://cataas.com/cat
      
    2. 点击代码上方出现的 Send Request

    3. 默认情况下,在右侧会打开 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 工作原理

  1. 客户端(如个人电脑)向服务端(如云服务器)发送请求(Request),请求包括:

    1. 请求方法(verb):希望服务端执行的操作,常见方法包括:
      1. GET:获取资源
      2. POST:创造资源
      3. PUT:更新资源
      4. PATCH:更新部分资源
      5. DELETE:删除资源
    2. 请求头(headers):关于请求的信息,请求的元数据,常见请求头包括:
      1. Content-Type:请求内容的类型
      2. Content-Length:请求内容的长度
      3. Authorization:请求者身份认证
      4. Accept:允许接受的类型
      5. Cookies:请求中的客户端数据
    3. 请求体(content):请求的内容,发送到服务端的数据,常见请求体包括:
      1. HTML、CSS、JavaScript、XML、JSON 等
      2. 内容对某些方法无效
      3. 帮助满足请求的信息
      4. 二进制信息

    举例:

    POST
    Content-Length: 11
    
    Hello World
    
    • 其中,POST 是请求方法;Content-Length: 11 是请求头;Hello World 是请求内容
  2. 服务端接收请求并处理后,向客户端发送响应(Response),响应包括:

    1. 状态码(status code):请求的执行状态,码类如下:

      1. 100-199:一般信息(Informational)
      2. 200-299:成功信息(Success)
      3. 300-399:重定向信息(Redirection)
      4. 400-499:客户端错误(Client Errors)
      5. 500-599:服务端错误(Server Errors)

      详细说明参考 HTTP 常见状态码说明 | SRIGT

    2. 响应头(headers):关于响应的信息,响应的元数据,常见响应头包括:

      1. Content-Type:响应内容的类型
      2. Content-Length:响应内容的长度
      3. Expires:响应有效期
      4. Cookies:请求携带的数据
    3. 响应体(content):响应的内容,常见响应内容包括:

      1. HTML、CSS、JavaScript、XML、JSON 等
      2. 二进制信息

    举例:

    201
    Contet-Type: text
    
    Hello World
    
  • 使用 REST Client 收发 HTTP 请求与响应

    1. 发送基本 GET 请求

      GET https://restdesign.dev
      
      GET https://restdesign.dev/css/site.css
      
    2. 将 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 有以下几个部分:

    • 请求
      1. 请求方法
      2. URI,统一资源标识符(Uniform Resource Identifier),包括查询字符串(Query String)参数
      3. 请求头
      4. 请求体,根据情况可选
    • 响应
      1. 状态码
      2. 响应头
      3. 响应体
  • 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 请求的响应
  • 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 进行缓存机制

    1. 客户端发起对指定版本的 GET 请求

      GET URI
      Version: last_xyz
      
      Hello World
      
    2. 服务端返回响应

      304 Not Modified
      
    3. 客户端发起 PUT 请求

      PUT URI
      If-Match: last_xyz
      
      Hello World
      
    4. 如果 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 流程如下:

    1. 客户端发起跨源请求

    2. 浏览器请求访问

      OPTIONS /api/user HTTP/1.1
      Origin: http://example.com
      Access-Control-Request-Method: POST
      Host: localhost:8080
      
      • Origin:目标源
      • Access-Control-Request-Method:请求访问的方法
      • Host:来源
    3. 服务端回复规则

      Access-Control-Allow-Methods: GET, POST, OPTIONS
      Access-Control-Allow-Origin: http://example.com
      Content-Type: 0
      
    4. 浏览器发出 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

  • 使用可信第三方验证

  • 服务端不会收到凭据

    • 用户通过第三方验证,使用令牌确认凭据
  • 工作流程:

    1. 开发者请求 API 密钥
    2. API 支持 API 密钥并分享密码
    3. 开发者请求一个令牌
    4. API 验证并返回令牌
    5. 开发者重定向到 API 的认证 URI
    6. API 显示授权 UI
    7. 用户确认授权
    8. API 重定向回到开发者
    9. 开发者通过 OAuth 请求访问令牌并请求令牌
    10. API 返回带有超时限制的访问令牌
    11. 开发者可以使用带有访问令牌的 API 知道超时

-End-

posted @ 2024-05-23 03:03  SRIGT  阅读(17)  评论(0编辑  收藏  举报