site logo

Marico's space

如何为多租户 Apify Actor 配置仅刷新令牌的 OAuth(Gmail,10 分钟)

编程技术 2026-05-20 14:50:02 10

最近折腾了 Apify Actor 接入 Gmail API 这件事,踩了几个坑,这篇把问题说清楚。

如果你要做一个调用用户级 Google API(Gmail、日历、Drive)的 Apify Actor,想用最简单的认证方案让陌生用户直接上手,这就是你要的方案。

核心思路:用户把三个字符串 —— refresh_tokenclient_idclient_secret —— 填到 Actor 输入框里。运行时 Actor 用这些换短期 access token,调用完 API 就退出。不需要邮箱缓存、不需要每用户 OAuth 回调地址、不需要 Apify 端存用户身份。

10 分钟能搞定,包括 Google Cloud 的配置。下面从头到尾演示一遍。这是我在生产环境 apify.com/foxck/gmail-inbox-intel 实际在跑的那套方案。

第一步 — Google Cloud 配置(5 分钟)

  1. 打开 console.cloud.google.com,新建一个项目或选已有的。
  2. APIs & Services → Enable APIs → 启用 Gmail API(或者你需要用的 Google API)。
  3. APIs & Services → OAuth consent screen → External,填应用名和你的邮箱,加上你实际需要的 scope(比如 gmail.readonly。如果用户数在 100 以内,把应用留在"Testing"模式,手动加测试用户就行 —— 不需要 Google 审核。
  4. APIs & Services → Credentials → Create credentials → OAuth client ID → Desktop app。下载 JSON 文件,这样你就有了 client_idclient_secret

"Desktop app" 这个类型是关键。它不需要你托管一个 OAuth 重定向 URL。Google 的库会在授权流程期间自动拉起 localhost:8080 来捕获 code。

第二步 — 生成刷新令牌(2 分钟)

用户在自己的机器上跑一次就行。不用你托管,让他们自己跑。

# scripts/oauth_setup.py
from google_auth_oauthlib.flow import InstalledAppFlow SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=8080, access_type="offline", prompt="consent")
print("refresh_token:", creds.refresh_token)

access_type="offline"prompt="consent" 是确保 Google 返回刷新令牌的关键。少了 prompt="consent" 的话,如果用户之前已经给这个客户端授权过,Google 就只会返回 access token。带上它,授权页面会重新弹出,一枚新的刷新令牌就到手了。

输出:一个以 1// 开头的长字符串。这就是 refresh_token。用户把它连同 client_idclient_secret 一起填到 Actor 输入框。

第三步 — 运行时换取 access token(1 分钟)

在 Actor 的主 handler 里,调用 Gmail 之前:

import requests def get_access_token(client_id, client_secret, refresh_token): resp = requests.post( "https://oauth2.googleapis.com/token", data={ "client_id": client_id, "client_secret": client_secret, "refresh_token": refresh_token, "grant_type": "refresh_token", }, timeout=10, ) resp.raise_for_status() return resp.json()["access_token"]

这个 access token 有效期大约 1 小时。对于一个运行几秒到几分钟的 Actor 来说,每次运行换一次就够用了 —— 不需要缓存。

第四步 — 定义 Actor 输入 schema(1 分钟)

INPUT_SCHEMA.json

{ "title": "Gmail Actor input", "type": "object", "required": ["client_id", "client_secret", "refresh_token"], "properties": { "client_id": { "type": "string", "title": "Google client_id", "editor": "textfield" }, "client_secret": { "type": "string", "title": "Google client_secret", "editor": "textfield", "isSecret": true }, "refresh_token": { "type": "string", "title": "Google refresh_token", "editor": "textfield", "isSecret": true } }
}

isSecret: true 这个标记告诉 Apify UI 在 Actor 运行记录中把字段值脱敏。Apify 会用平台级密钥对 secret 输入字段自动加密。

第五步 — 加分项:dry-run 模式让用户不用配 OAuth 就能测试(1 分钟)

冷用户上手最大的卡点就是第一步里 Google Cloud Console + OAuth 客户端 + 授权页面那一套流程。用户刚点开你的 Actor,点击运行,遇到 OAuth 墙,直接放弃。

解法:一个可选的 dry_run 布尔值。开启时,Actor 跳过 OAuth 换取步骤,直接输出一个符合真实 schema 的模拟数据集:

async def main(): inp = await Actor.get_input() if inp.get("dry_run"): await Actor.push_data(SYNTHETIC_SAMPLE) return # ... 正常流程,包含 OAuth ...

现在用户第一次交互变成"点公开示例的运行 → 看看输出长什么样"。不用碰 Cloud Console。只有看完 JSON 格式、决定想要这个数据之后,才会去走 OAuth 流程。

为什么选这个方案,而不是其他的

  • Apify Integrations OAuth:把 Actor 绑到 Apify 的身份存储里,会把想自托管的用户拦在外面。
  • Service Account + 域范围委派:只适用于一个 Google Workspace,对多租户陌生用户不适用。
  • 每用户 OAuth 回调 URL 托管在某处:额外的 infra、额外的成本、额外的故障点。

仅刷新令牌 OAuth 把信任边界划得很清楚:用户持有自己的凭证,Actor 是无状态的,Apify 和自托管模式都能用同一套代码跑。

仓库地址

生产环境跑着这套方案的完整源码:github.com/foxck016077/apify-gmail-inbox-intel(MIT 协议)。Actor 主页在 apify.com/foxck/gmail-inbox-intel —— 点"Try for free",输入 dry_run: true,不用配 OAuth 就能看到输出格式。

如果你也在做类似的东西、方案哪里不清楚,AMA discussion 敞着,随时来聊。

原文链接:https://dev.to/