跳转到内容

请求、接口与鉴权

后端最基础的工作,是接收请求、确认身份、执行规则、读写数据并返回可解释的结果。框架只是把这些步骤组织起来。真正困难的是边界:什么算合法输入,谁有权执行,操作失败后应该留下什么状态,客户端怎样知道是否成功。

1. 一次请求的完整路径

跳转到“1. 一次请求的完整路径”
客户端
│ URL、方法、头部、请求体
▼
DNS → TCP → TLS → 入口代理/负载均衡
│
应用服务器
│ 解析 HTTP → 中间件 → 路由
▼
控制器 → 业务服务 → 数据访问 → 外部依赖
│
▼
响应:状态码 + 头部 + 响应体

每一步都可能失败。域名没解析、连接被拒绝、证书过期、代理返回 502、路由不存在、鉴权失败、数据库超时,最终都可能表现为“接口不能用”,但处理方式完全不同。

中间件位于请求进入业务代码之前,或响应离开之后,常负责:

  • 生成请求 ID,建立日志上下文。
  • 解析 Cookie、认证信息和内容类型。
  • 限制请求体大小、限流和检查 IP/租户。
  • 压缩、CORS、安全头部和缓存控制。
  • 捕获异常,把内部错误转换成稳定响应。

中间件顺序会改变行为。例如鉴权必须在业务路由前,日志上下文要尽早建立,错误处理通常放在最外层。顺序错误可能让未认证请求先触发昂贵查询,或让错误响应缺少请求 ID。

1.2 控制器、服务与数据访问

跳转到“1.2 控制器、服务与数据访问”
  • 控制器负责 HTTP 边界:读取参数、调用服务、选择状态码。
  • 服务层负责业务规则:权限、状态转换、事务和多个操作的组合。
  • 数据访问层负责查询与持久化,不决定业务是否允许。

把数据库查询、权限判断和响应拼装全塞进控制器,代码短期能跑,但很难测试和复用。不是每个小接口都必须分三层;边界复杂时再拆分,但不要把不同职责混成一个不可解释的函数。

HTTP 是请求/响应协议。请求包含方法、目标、版本、头部和可选请求体;响应包含状态码、头部和响应体。

POST /api/orders HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token>
{"items":[{"sku":"A-1","quantity":2}]}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/orders/ord_123
{"id":"ord_123","status":"pending"}
方法 语义 是否应幂等
GET 读取资源 是
HEAD 读取响应头 是
POST 创建资源或提交操作 不要求
PUT 整体替换目标资源 是
PATCH 部分修改资源 取决于设计
DELETE 删除资源 通常是

方法语义会影响缓存、重试、代理和客户端行为。不要把所有操作都塞进 GET;GET 不应产生副作用,也不应携带密码等敏感信息到 URL 和日志中。

只需掌握分组和常用码:

  • 2xx:成功。200 一般成功,201 已创建,204 成功但无响应体。
  • 3xx:重定向或缓存相关。301/308 永久重定向,302/307 临时重定向。
  • 4xx:请求或权限问题。400 参数错误,401 未认证,403 已认证但无权限,404 不存在,409 状态冲突,422 语义校验失败,429 限流。
  • 5xx:服务端未能完成。500 未分类错误,502 上游无效响应,503 暂时不可用,504 上游超时。

状态码不是装饰。错误分类正确,客户端才能决定重试、登录、提示用户还是停止。

常见头部及用途:

头部 作用
Content-Type 请求或响应的数据格式
Accept 客户端希望接收的格式
Authorization 凭据,如 Bearer token
Cache-Control 缓存策略
ETag / If-None-Match 条件请求与版本校验
Idempotency-Key 让重复写请求可安全重试
X-Request-Id 跨服务追踪请求
Set-Cookie 服务端设置 Cookie
Retry-After 限流或暂不可用时建议等待时间

自定义头部也要有明确语义,并考虑代理是否透传、日志是否会记录敏感值。

接口文档不是“写给别人看的附件”,而是客户端与服务端的契约。契约至少包括:

  • 路径、方法、鉴权方式和权限。
  • 输入字段、类型、必填、范围和默认值。
  • 成功响应结构与状态码。
  • 稳定错误码、错误消息和字段错误。
  • 分页、排序、过滤和幂等规则。
  • 超时、重试、限流和长任务行为。

REST 风格通常用名词表示资源:

GET /api/orders?status=paid&cursor=...
GET /api/orders/{orderId}
POST /api/orders
PATCH /api/orders/{orderId}
DELETE /api/orders/{orderId}

不要在 URL 中写动词式的 getOrder,除非该操作确实不是资源操作,例如 POST /api/orders/{id}/cancel 表示一次有业务语义的转换。

幂等表示同一请求重复执行,最终业务效果应当一致。GET、PUT、DELETE 通常设计为幂等,POST 创建请求则容易制造重复。

客户端超时后不知道请求是否成功,可能重试。如果服务端每次重试都创建新订单,就会产生重复副作用。解决办法包括:

  • 客户端生成 Idempotency-Key,服务端保存处理结果。
  • 数据库使用唯一约束或业务幂等键。
  • 写操作先检查状态,再决定是否执行。
  • 对无法确认结果的场景返回可查询的操作 ID。

“重试三次”不能替代幂等设计。重试只会让重复更明显。

偏移分页简单:

GET /api/orders?page=3&pageSize=20

但数据频繁插入时会出现重复或跳过。游标分页用稳定排序字段和位置:

GET /api/orders?limit=20&cursor=eyJjcmVhdGVkQXQiOi...

游标需要可验证、不可伪造成越权位置;排序字段要稳定,通常加上唯一 ID 作为最后比较项。

版本变化不只是在路径里加 v2。字段新增、枚举扩展、默认值改变和错误结构变化都涉及兼容:

  • 新增可选字段通常向后兼容。
  • 删除字段、改类型、改语义通常是破坏性变更。
  • 枚举增加新值可能让旧客户端崩溃。
  • 时间、金额和大整数要明确格式与精度。
  • 新客户端调用旧服务端时,服务端应忽略未知输入还是拒绝?

优先设计可演进的 schema,再决定是否需要版本化。版本化会带来多套逻辑和迁移成本,不是免费的隔离墙。

风格 优势 代价
REST/JSON 通用、可读、生态广 灵活查询复杂时接口会膨胀
GraphQL 客户端按需取字段,减少多次请求 查询复杂度、缓存、权限与限流更复杂
gRPC/Protobuf 强 schema、二进制高效、适合内部调用 浏览器支持、调试和网关适配成本
Webhook 事件驱动,不用轮询 重复投递、签名、重试和顺序问题
文件/流 上传下载、实时结果 范围请求、背压、断点续传和资源释放

选型看调用方、数据规模和故障模式,不看向量图。公开集成优先考虑通用性,内部高频服务可能更重视 schema 与性能。

4.1 Webhook 也必须有契约

跳转到“4.1 Webhook 也必须有契约”

Webhook 是“服务器主动调用你的服务器”。要处理:

  • 签名验证和时间戳,防止伪造与重放。
  • 事件 ID 与幂等处理。
  • 超时、重试和死信。
  • 乱序到达与重复投递。
  • 版本字段与向后兼容。

接收方快速返回已接受不代表业务处理完成,异步任务状态需要可查询。

序列化把内存对象变成可传输字节,反序列化把字节还原成对象。格式选择影响可读性、体积、schema、兼容性和安全。

格式 特点 适合
JSON 可读、通用、弱 schema 公开 API、配置文件
Protobuf 紧凑、强 schema、代码生成 服务间 RPC、高吞吐
MessagePack 二进制、较紧凑 内部接口、特定客户端
CSV 表格简单、工具广 数据导入导出
自定义二进制 可控、极高效率 音视频、协议、底层系统

无论格式,都要处理:

  • 未知字段是忽略、保留还是拒绝。
  • 数字精度、时间格式、空值和默认值。
  • 字符编码与最大长度。
  • schema 版本和迁移。
  • 反序列化不可信数据时的资源与安全风险。

JSON 没有原生日期和精确十进制类型;大整数可能超过 JavaScript 安全整数范围。对外传输大 ID 时可以使用字符串,金额使用最小货币单位整数或明确的字符串格式。

认证回答“你是谁”,授权回答“你能做什么”。两者不能混在一起,也不能只在前端执行。

服务端创建会话,浏览器保存 Cookie,请求时自动携带。关键属性:

  • HttpOnly:JavaScript 不能读取,降低 XSS 窃取风险。
  • Secure:只通过 HTTPS 发送。
  • SameSite:限制跨站携带,降低 CSRF。
  • Path、Domain、Expires:控制有效范围。

Session 适合传统 Web 应用,服务端可以撤销会话,但要处理存储和水平扩展。

JWT 把声明和签名放在令牌里,服务端可无状态验证。需要注意:

  • 验证签名算法和密钥,不接受 none 或错误算法。
  • 检查过期时间、签发者、受众和用途。
  • 不把敏感信息放进 payload,因为它只是编码,不是加密。
  • 撤销困难时,使用短有效期、刷新令牌或服务端黑名单。

JWT 不是“更安全”的默认答案,只是把状态放到不同位置。

OAuth 主要解决第三方授权,OIDC 在其上提供身份信息。授权码流程要使用 state 防止请求伪造,PKCE 防止授权码被截获。回调地址必须白名单匹配,scope 要最小化。

API Key 适合机器调用和服务级配额,但要:

  • 只在安全通道传输。
  • 可以轮换、撤销和限制权限。
  • 不放在前端、URL 或公开日志里。
  • 与用户身份、租户和速率限制关联。
模型 例子
RBAC 管理员、编辑、只读角色
ABAC 根据部门、资源标签、时间等属性判断
资源级权限 只有文档所有者或协作者可以编辑
租户隔离 用户只能访问所属租户的数据

每个受保护资源都要检查对象级权限。用户已登录不代表可以读取任意订单;修改 URL 中的 ID 后仍必须由后端决定是否允许。

7. 输入校验与错误契约

跳转到“7. 输入校验与错误契约”

所有来自外部的数据都应视为不可信:请求体、查询参数、路径、头部、Cookie、上传文件名、第三方回调。校验至少包括:

  • 类型、必填、长度、范围、格式。
  • 业务状态是否允许此操作。
  • 身份和对象级权限。
  • 资源大小、分页上限和文件类型。

错误响应应稳定、机器可读,同时不泄露内部实现:

{
"error": {
"code": "ORDER_NOT_PAYABLE",
"message": "当前订单不能支付",
"fields": {},
"requestId": "req_123"
}
}

生产环境不要返回堆栈、SQL、内部主机名、密钥或完整第三方响应。日志可以记录更详细的原因,但要脱敏并关联 request ID。

8. 长请求、上传与流式响应

跳转到“8. 长请求、上传与流式响应”

普通请求应尽快返回,避免连接长时间占用。耗时任务可以:

  1. 立即返回任务 ID。
  2. 把任务放入队列。
  3. 由工作进程处理并更新状态。
  4. 客户端轮询、SSE 或 WebSocket 获取进度。
  5. 提供取消、重试和结果过期策略。

文件上传要考虑:

  • 请求体大小限制和流式处理。
  • 文件类型、扩展名与实际内容校验。
  • 对象存储直传与临时签名 URL。
  • 断点续传、分片和范围请求。
  • 文件名注入、病毒扫描、配额与清理。

流式响应要定义“已经发送了多少数据后失败”的语义。客户端收到部分内容后再重试,可能重复执行或拼接错乱。

Web 框架通常提供路由、中间件、请求解析、响应序列化和依赖注入。选择时看:

  • 团队语言经验与生态。
  • 请求处理模型(同步、异步、事件循环)。
  • 类型与 schema 支持。
  • 中间件、认证、数据库和测试工具。
  • 部署方式、内存与启动时间。
  • 社区维护和安全更新。

不同语言框架的差异,通常不如清晰的输入校验、错误边界、权限模型和可测试性重要。框架能让第一个接口很快跑起来,但无法替你决定事务、幂等和权限。

  1. 每个接口是否有明确输入、输出、错误码和权限。
  2. 写操作是否有幂等策略,重试不会制造重复副作用。
  3. 认证与授权是否都在服务端执行,是否检查对象级权限。
  4. Cookie、令牌、API Key 是否在正确位置,是否有轮换与撤销。
  5. 分页、排序、过滤是否有上限,是否可稳定遍历。
  6. 序列化格式是否处理精度、时间和 schema 兼容。
  7. 长请求、上传、流式响应和取消是否定义。
  8. 错误是否可定位,同时不泄露内部细节。
  9. 日志是否有 request ID,敏感字段是否脱敏。
  10. API 文档与真实行为是否一致。