Vincent's Essays

博客园 首页 新随笔 联系 订阅 管理

Memoria 开发记录 25:把 API Key 从客户端拿走——Cognito 鉴权与 Lambda 代理

前言

移动端应用接入第三方服务时,最容易留下的技术债就是把 API Key 写进本地配置。开发阶段这样做很快:高德、DeepSeek、音乐生成、图片生成等服务都可以直接用一个 dev.json 跑起来。但只要应用需要分发,这种方式就变成了明显风险。

客户端是用户设备上的代码和资源,不能被当成可信执行环境。即使配置文件不上传仓库,只要 token 随安装包、日志、备份或调试产物存在,就有泄漏可能。更严重的是,一旦 key 泄漏,服务侧很难区分请求来自真实用户还是被复制出去的脚本。

因此 Memoria 最近将云端 API 访问改成 Cognito 鉴权 + AWS Lambda 代理:客户端只拿自己的登录态,请求由 Lambda 代持真实 provider key。

为什么不是继续加密本地 token

把 key 加密后放进客户端,本质上只是把明文泄漏变成“需要多走一步”。解密逻辑仍然在客户端,密钥也必须以某种形式随应用存在。对于逆向和动态调试来说,这类防护只能增加一点成本,不能改变信任边界。

更合理的边界是:

客户端:
  只持有 Cognito 用户身份和短期 token

Lambda:
  校验 Cognito token
  读取 Secrets Manager 中的 provider key
  调用第三方 API
  返回裁剪后的结果

这样第三方 key 不出现在客户端,也不需要在多个平台配置多份本地 secret。

代理不是通用 URL 转发器

安全代理不能做成“给我一个 URL,我帮你请求”。那样等于开放了一个带鉴权的 SSRF 工具,还会让成本、权限和日志不可控。

Memoria 的代理采用白名单路由:

GET  /v1/amap/regeo
GET  /v1/amap/place/text
POST /v1/llm/chat/completions
POST /v1/replicate/predictions
GET  /v1/replicate/predictions/{id}

每个路由只转发必要参数,并且由 Lambda 自己拼接上游 URL 和 provider token。这样可以明确知道每个接口允许什么输入、可能产生什么成本,以及需要怎样的错误处理。

Cognito 的作用

Cognito 不是为了“隐藏接口地址”。API Gateway 地址仍然可以公开。Cognito 的作用是让后端知道请求来自已登录用户,并且可以在未来扩展限流、配额、审计和用户级权限。

客户端侧需要处理两个问题:

  • 启动时正确初始化 Amplify Auth;
  • 请求失败时区分“未登录”“token 过期”“服务端失败”和“第三方失败”。

之前如果 Auth 插件没有注册,业务层可能只看到某个页面一直加载。现在更好的做法是把 token 获取封装成独立服务,401 时主动刷新或重新获取用户池 token,失败则抛出明确错误。

部署脚本也属于产品路径

这次接入过程中还暴露了一个部署脚本问题:使用 -SkipSecretUpsert 时,脚本不能把 CloudFormation 参数 SecretId 传空。跳过 secret 更新的语义应该是“不改 secret 内容”,而不是“清空 Lambda 对 secret 的引用”。

这类问题说明,后端代理不是写完 Lambda 就结束。部署脚本、Secrets Manager、CloudFormation 参数和本地 profile 都是同一条产品链路的一部分。

最终脚本需要满足:

  • 首次部署可以创建 secret;
  • 后续部署可以只更新代码,不覆盖 secret;
  • 跳过 secret 更新时仍保留已有 SecretId
  • 输出稳定 API endpoint;
  • smoke test 可以验证 Cognito 和代理路由是否可用。

错误处理不能只返回 500

代理最初将上游失败直接变成 500,客户端日志只能看到 Dio 的 bad response。这对调试几乎没有帮助。

后续改造中,代理对 Amap 请求增加了:

  • 上游 fetch 超时控制;
  • 简单重试;
  • IPv4 优先;
  • place/text 失败后的 geocode/geo 兜底;
  • 服务端错误体保留 message
  • 客户端日志打印状态码和响应体。

这不是为了让失败消失,而是让失败可诊断。移动端排查云端问题时,最怕“没有找到结果”和“服务坏了”被混成同一种空状态。

总结

把 API Key 从客户端拿走,不只是一个安全优化,也是一次架构边界调整。客户端负责用户身份和交互,Lambda 负责可信转发和 provider key 管理,Secrets Manager 负责敏感配置。

关键经验包括:

  • 客户端不能被当成 API Key 的安全存储;
  • Cognito 解决的是用户身份和授权边界;
  • Lambda 代理应该使用白名单路由,而不是通用 URL 转发;
  • 部署脚本必须保留 secret 引用,不能因为跳过更新而清空配置;
  • 代理错误要可诊断,不能把一切都伪装成 500;
  • 本地 profile 中应只保留 endpoint、模型名和 Cognito 配置,不再保留 provider token。

对应提交:cb3d73a0295124d5838acbda337207c235e

posted on 2026-06-17 01:05  Vincentson  阅读(2)  评论(0)    收藏  举报