移动 API 网关应承担无法立即发布到每个已安装 App 的服务端策略与兼容工作:验证访问令牌、限制负载、路由受支持的 API 版本,并输出一致的故障信号。它不能把 App 当作机密客户端,也无法保证网络连通,更不能通过无限期转换掩盖后端破坏性变更。
核心约束是版本发布滞后。Web 客户端可以随服务端一起更新,移动端旧版本却可能继续运行数月。因此,网关与 API 契约必须支持多代客户端并存。
核心要点
- 原生 App 是 OAuth 公共客户端,无法安全保存全局共享的客户端机密信息。
- 使用系统浏览器或其他外部 User-Agent,并采用带 PKCE 的授权码流程。
- 验证 Token 的 Issuer、Audience、有效期与 Scope,再执行应用授权;设备请求头不是身份。
- 针对多变网络优化负载结构与大小,同时明确缓存与压缩语义。
- 围绕兼容性承诺设计版本,观测采用率,并通过可测量策略下线旧版本。
分开移动端与 API 信任边界
1sequenceDiagram
2 participant U as 用户
3 participant M as 移动 App
4 participant B as 外部浏览器
5 participant A as 授权服务器
6 participant G as API 网关
7 participant S as 移动后端
8 U->>M: 开始登录
9 M->>B: 授权请求与 PKCE Challenge
10 B->>A: 用户认证与授权同意
11 A-->>M: 通过已注册 Redirect 返回授权码
12 M->>A: 授权码与 PKCE Verifier
13 A-->>M: 访问令牌
14 M->>G: API 请求与访问令牌
15 G->>G: 验证 Token 与路由策略
16 G->>S: 最小已验证上下文
17 S->>S: 授权用户与对象
18 S-->>G: 带版本的响应
19 G-->>M: 有边界的移动端响应RFC 8252要求原生 App 的授权请求使用外部 User-Agent,并要求公共原生客户端和授权服务器使用 PKCE。RFC 7636要求发起 App 提交自己掌握的 Verifier,从而防止被截获的授权码遭到利用。
不要在 App 中嵌入共享的机密客户端密钥。攻击者可以从已分发的二进制文件中提取它。设备型号、操作系统版本、App 版本、推送 Token、IP 地址和 Attestation 结果可以作为风险策略信号,但其中任何一个都不能单独证明用户身份。
验证 Token,再授权资源
网关应按照授权系统选定的机制验证:
- 签名或 Introspection 结果;
- 精确的 Issuer 与目标 Audience;
- Expiration 与 Not Before,并配套有文档的时钟策略;
- 路由要求的 Scope;
- 生态采用发送方约束时,对应的密钥持有证明。
然后通过经过认证的内部路径传递最小的已验证主体和策略上下文。后端仍需授权具体账户、对象与操作。有效 Token 并不代表拥有所有访问权。
对于移动 App 直接调用的 API,Apache APISIX 3.19.0 的 openid-connect 插件可以使用 bearer_only: true。该字段默认为 false:false 路径支持网关侧授权码与 Session 流程,true 路径则严格要求 Bearer Token。原生授权流程应由 App 与授权服务器完成;网关路由应作为资源服务器,而不是把浏览器 Session 契约嵌入 API。
bearer_only 本身不会验证 Token Audience。在 APISIX 3.19.0 中,claim_validator.audience.required 与 claim_validator.audience.match_with_client_id 均默认为 false:false 路径既不要求 aud,也不把它与插件 client_id 匹配。如果该客户端 ID 就是预期资源 Audience,应明确把两者都设为 true;此时 APISIX 会要求 aud 存在,并拒绝与客户端 ID 不同的字符串,或不包含该值的数组。如果移动 API 使用其他 Audience 模型,应配置并测试准确契约,而不能假设 Bearer 模式自动提供验证。
为不稳定网络限制负载
移动客户端会遇到高延迟、带宽受限、无线网络切换与上传中断。网关可执行粗粒度限制,但负载设计仍应在 API 层解决:
- 对集合分页,只返回客户端需要的字段;
- 对未变化的资源使用条件请求与 Validator;
- 只有节省的字节值得付出 CPU 与延迟代价时才压缩;
- 使用短期授权,把大型媒体直接上传到专用服务;
- 避免让一个“移动端”接口无界扇出到许多服务;
- 返回机器可读错误,区分认证、授权、验证、过载与版本退役。
下面的 APISIX 3.19.0 配置片段把一个上传路由限制为 1 MiB。client-control 插件要求 APISIX-Runtime,因此使用前必须确认目标构建。
1{
2 "uri": "/mobile/v2/profile/photo",
3 "methods": ["POST"],
4 "plugins": {
5 "client-control": {
6 "max_body_size": 1048576
7 }
8 },
9 "upstream": {
10 "type": "roundrobin",
11 "nodes": {
12 "mobile-backend.internal:8080": 1
13 }
14 }
15}对 max_body_size 而言,正整数会执行对应字节上限,超大请求体返回 HTTP 413;特殊值 0 会关闭这项检查。网关限制只是其中一层:App 应提前检查媒体大小,上传服务还必须独立验证实际类型、尺寸、所有权与存储策略。
选择版本契约
路径、媒体类型或请求头版本都可以工作。应选用一种让客户端、文档、分析与支持工具都能一致识别的约定。
| 方式 | 优点 | 需要管理的风险 |
|---|---|---|
/v2 等路径 | 在日志、链接和路由中清晰可见 | 容易复制出大量重复 API |
| 媒体类型或请求头 | 资源 URI 保持稳定 | 正确检查与缓存更困难 |
| 能力协商 | 支持渐进式功能 | 可能产生无法全面测试的组合矩阵 |
在受支持版本内,应优先进行向后兼容的增量变更。必须进行破坏性变更时,应让新旧契约并行运行,公布支持结束日期,测量活跃客户端版本,并向客户端返回可操作的升级响应。网关转换可以帮助完成范围有限的迁移,但不应成为没有文档的第二套业务 API 实现。
不要只依据调用方提供的 App 版本请求头路由,再把该值当作已验证的软件完整性。它可以在明确限制后用于兼容路由与遥测,但不是官方二进制发起请求的证明。
设计离线与重试行为
服务端已经提交写入后、响应到达前,App 可能失去网络。对于会改变状态的操作:
- 由 App 生成唯一操作 Key;
- 服务端将 Key 与已认证用户和请求指纹绑定;
- 相同操作再次到达时返回已存储结果;
- 为结果不确定的操作提供状态查询;
- 只有理解离线与重试窗口后,才设置 Key 过期时间。
不要让网关悄悄重试所有 POST。RFC 9110禁止代理自动重试非幂等请求。协调客户端、网关、服务与第三方的重试,确保它们叠加后的尝试次数仍有明确上限。
运维版本弃用窗口
按经过隐私审查的 App 版本、API 版本、平台、路由和结果跟踪活跃用户与流量。退役前应:
- 确认认证与最低版本策略不会阻断应急访问;
- 使用旧客户端测试新的身份和证书轮换;
- 提供稳定错误码、升级 URL 与宽限策略;
- 为网关路由保留回滚路径;
- 在公布的窗口结束后移除转换与路由,而不是永久维护。
验证清单
- 原生登录是否使用外部 User-Agent 与 PKCE?
- 是否错误地把任何嵌入式机密信息视为保密凭据?
- 网关是否验证 Issuer、Audience、时间、Scope 与选定的证明机制?
- 后端是否仍会授权用户与对象?
- 上传与响应大小是否在所有相关层级受到限制?
- 客户端能否区分离线导致的不确定状态与已确认失败?
- POST 重试是否具备幂等性且次数有界?
- 能否观测、回滚并有计划地退役新旧 API 版本?
常见问题
移动 App 是否应该直接在 API 网关认证?
App 通常从授权服务器获取 Token,再把它提交给作为资源服务器的网关。网关验证 Token,不应收集用户密码。
App 版本请求头可信么?
不可信。除非存在独立验证机制把它绑定到经过 Attestation 的客户端,否则它由调用方提供。使用它进行兼容路由和遥测时,必须明确这一限制。
是否应该压缩每个移动端响应?
不应该。压缩适合体积足够大且可压缩的内容,但会消耗 CPU,对小负载或已经压缩的媒体帮助不大。应按负载类型与网络条件测量。
