三天时间,一个五分钟能解决的问题。根本原因是:Drive "Open With" 的 OAuth 请求压根不是你的服务器发起的。
背景:从 ngrok 调试到生产环境出了问题
最近在给自己的文档处理工具Renomee AI接入 Google Drive 集成——用户可以在 Drive 里右键文件 → "打开方式" → 选我的 App,然后触发 OAuth 授权、处理文件。
整体架构如下:
┌──────────────┐ ┌─────────────────────┐ ┌──────────────┐
│ Google Drive │ │ drive-api.renomeeai │ │ Google APIs │
│ (用户端) │ │ (FastAPI) │ │ │
└──────┬───────┘ └──────────┬───────────┘ └──────┬───────┘
│ │ │
│ 1. 右键文件 → "打开方式" │ │
│ Drive 构造 OAuth URL │ │
│──────────────────────────►│ │
│ │ │
│ │ 2. 重定向 → Google 授权页 │
│◄───────────────────────────┼────────────────────────────│
│ 用户点击允许 │ │
│ │ 3. callback: code 换 token│
│ │───────────────────────────►│
│ │◄───────────────────────────│
│ │ access_token+refresh_token│
│ │ │
│ │ 4. 加密存 DB,下发 Cookie │
│◄───────────────────────────│ │
│ │ │
│ 5. 前端调 session/init │ │
│──────────────────────────►│ │
│ │ 6. 用 token 拉文件元数据 │
│ │───────────────────────────►│
│ │◄───────────────────────────│
│◄───────────────────────────│ files: [name, mimeType, │
│ { needs_auth, files } │ imageMetadata] │
│ │ │
│ 7. 输入重命名指令 │ │
│──────────────────────────►│ │
│ │ 8. 调 RenameAI 获取建议名 │
│ │──────► api.renameai.com │
│ │◄────── suggested_name │
│◄───────────────────────────│ │
│ { suggested_name } │ │
│ │ │
│ 9. 用户确认 → apply │ │
│──────────────────────────►│ │
│ │ 10. PATCH Drive 文件名 │
│ │───────────────────────────►│
│◄───────────────────────────│◄───────────────────────────│
│ { success, new_name } │ │
本地开发阶段用 ngrok 做内网穿透,在 Google Cloud Console 里把 ngrok 临时地址注册为 Redirect URI,跑通了整个流程。
切换到生产环境时,我做了三件事:
- 在 GCP Console 把 Redirect URI 更新为正式域名
- 改了服务器
.env里的GOOGLE_DRIVE_REDIRECT_URI - 重建 Docker 容器重新部署
测试时:
Error 400: redirect_uri_mismatch
The redirect URI in the request did not match a registered redirect URI.
打开 F12 → Network,找到跳转到 Google 授权页面前的那条请求,URL 里赫然写着:
redirect_uri=https://7886-45-78-56-186.ngrok-free.app/drive/open
是几个月前的 ngrok 旧地址。.env 改了,容器重建了,但这个地址没变。
排查过程:先搞清楚这个 URL 是谁构造的
直觉上以为是我服务器发出的请求,所以一直在改代码和配置。这个思路本身就错了。
实际上,Drive "Open With" 有两套完全独立的 OAuth 流程:
| 流程 | OAuth URL 由谁构造 | redirect_uri 来自哪里 |
|---|---|---|
你的 /auth/start 路由 |
你的服务器 | .env 配置 |
| Drive "Open With" 原生跳转 | Google Drive 的服务器 | Drive UI Integration 配置 |
F12 里抓到的那个 URL,是 Google 的服务器根据我当初注册 App 时的配置自动生成的。我的代码、我的 .env、我的容器,对它没有任何影响。
第一步:F12 提取真实的 redirect_uri
在排查类似问题时,第一步永远是先拿到"实际发出的 URL",而不是看代码里写了什么。
- Google Drive 右键文件 → 打开方式 → 点你的 App
- 报错页面出现之前,打开 DevTools(F12)→ Network 标签
- 过滤
accounts.google.com - 找到那条跳转请求,复制完整 URL
你会看到一个类似这样的完整 URL:
https://accounts.google.com/o/oauth2/auth
?scope=https://www.googleapis.com/auth/drive.file
&client_id=YOUR_CLIENT_ID.apps.googleusercontent.com
&response_type=code
&access_type=offline
&redirect_uri=https://7886-45-78-56-186.ngrok-free.app/drive/open ← 问题就在这里
&state=%7B%22ids%22%3A%5B%22xxx%22%5D%2C%22action%22%3A%22open%22%2C%22userId%22%3A%22xxx%22%7D
注意 redirect_uri 参数——如果它是一个旧的 ngrok 地址或其他已失效的域名,说明这个 URL 不是你的服务器构造的,而是 Google 服务端根据历史注册信息生成的。
用脚本提取 redirect_uri:
python3 -c "
import urllib.parse as u
url = '粘贴完整的 URL'
qs = u.parse_qs(u.urlparse(url).query)
print('redirect_uri:', qs.get('redirect_uri', ['未找到'])[0])
"
如果看到的是旧地址、ngrok 地址或其他错误域名,继续往下。
第二步:查询 Drive App Registry(权威数据源)
GCP Console 显示的是"你提交的配置"。Drive API 的 apps.get 接口返回的,才是"Google Drive 服务器实际在用的配置"。两者之间存在同步延迟,有时甚至永远不同步。
通过 OAuth Playground 获取 Access Token
- 打开 developers.google.com/oauthplayground
- 右上角 ⚙️ → 可选填入自己的 Client ID / Secret
- 左侧 Step 1 输入 scope,点 Authorize APIs:
https://www.googleapis.com/auth/drive.apps.readonly - 完成授权后点 Exchange authorization code for tokens
- 复制 Access token
查询你的 App 配置
TOKEN="你的 access token"
APP_ID="YOUR_PROJECT_NUMBER" # GCP 项目编号(纯数字),不是项目 ID
curl -s \
-H "Authorization: Bearer $TOKEN" \
"https://www.googleapis.com/drive/v3/apps/$APP_ID?fields=id,name,openUrlTemplate,installed,authorized" \
| python3 -m json.tool
GCP 项目编号(
projectNumber)和项目 ID(projectId)是两回事,用gcloud projects describe YOUR_PROJECT_ID --format="value(projectNumber)"获取。
在 openUrlTemplate 字段找到了问题所在。
我的响应是这样的:
"openUrlTemplate": "https://accounts.google.com/o/oauth2/auth?scope=...&redirect_uri=https://7886-45-78-56-186.ngrok-free.app/drive/open&..."
Google 把我几个月前的 ngrok 地址硬编码进了这个模板,存在它的服务端。每次用户触发"打开方式",Drive 就用这个模板构造 OAuth URL,所以我怎么改本地配置都没有用。
第三步:找到真正的配置入口
Google Cloud Console
→ APIs & Services
→ Enabled APIs & Services
→ Google Drive API(点击进入)
→ Drive UI Integration 标签页
问题根源:一个被废弃的选项
在这个页面里找这个复选框:
☐ Automatically show OAuth 2.0 consent screen when users open
my application from Google Drive
我的 App 勾选了这个选项。这是根本原因之一。
这个选项勾选后,Drive 不会把用户直接发送到你的 Open URL,而是先构造一个 OAuth 包装 URL,其中的 redirect_uri 就是你当时注册时填的地址(在我的情况下是 ngrok)。这个包装 URL 会被写入 Drive App Registry 的 openUrlTemplate 字段,存储在 Google 服务端。
Google 已经正式废弃了这个选项,但很多老教程里仍然推荐勾选它,踩坑的人因此前赴后继。
第一步:取消勾选,保存,同时确认 Open URL 正确。
Open URL: https://your-domain.com/drive/open
⚠️ 注意:这只是必要条件,不是充分条件。
取消勾选后,你的"提交配置"是正确的了,但 Drive App Registry 里已经存了旧的
openUrlTemplate。这两件事是独立的: 复选框控制"未来是否继续生成包装 URL",Registry 缓存控制"现在实际生效的 URL"。保存后必须用
apps.get验证 Registry 是否已经同步,不能只看 Console 页面。
第四步:验证配置是否同步到 Registry
保存后,每隔 10 分钟重新跑一遍 Step 2 的 apps.get 查询。
期望看到的响应:
"openUrlTemplate": "https://your-domain.com/drive/open?state={ids}&action={action}&userId={userId}"
不再有 OAuth 包装 URL,不再有旧地址。
如果超过 1 小时仍然是旧值,不要再等了。 直接跳到文末"当一切方法都无效时"章节。旧项目的 Registry 有可能就是不会同步,继续等只是浪费时间。
第五步:重新安装 App
这里还有一个坑:即使 Registry 里的配置已经更新,之前用旧配置安装过 App 的账号,仍然在使用旧的安装状态。必须完整走一遍卸载-重装流程。
正确的重装顺序(顺序不能错):
- Google Drive 设置 → 管理应用 → 找到你的 App → 断开连接
- myaccount.google.com/connections → 找到 App → 移除访问权限
- 重新执行
apps.get,确认installed: false - 等到
apps.get返回新配置后,再重新触发授权 - 如果在第 4 步完成前就重新授权,会再次安装旧配置
第 4 步是个隐性依赖。我在第一次修复后跳过了这一步,导致安装了旧配置,又花了大半天才发现。
附:新增文件类型不出现在菜单
在修复 OAuth 问题的同时,我还需要给 App 增加图片支持(jpeg、png 等)。MIME 类型加到 Console 里之后,图片文件的右键菜单里就是不出现我的 App。
原因:已安装的 App 实例不会自动拉取新的 MIME 类型配置,必须按上面的顺序完整走一遍卸载重装。
另外,文件类型建议放在 Secondary(次要)而不是 Default(默认),避免把你的 App 设置为默认打开方式:
Secondary MIME Types:
application/pdf
image/jpeg / image/png / image/webp
Secondary File Extensions:
pdf / jpg / jpeg / png / webp
当一切方法都无效时:新建 GCP 项目
如果保存配置超过 1 小时,apps.get 仍然返回旧的 openUrlTemplate,说明这个项目的 App Registry 条目可能陷入了一种卡死状态。
方式 A:禁用再启用 Drive API(有时有效)
gcloud services disable drive.googleapis.com --project=YOUR_PROJECT_ID
gcloud services enable drive.googleapis.com --project=YOUR_PROJECT_ID
重新保存 Drive UI Integration,再查询。不保证有效。
方式 B:新建 GCP 项目(推荐,这才是真正解决问题的方式)
在旧项目上耗了将近一天之后,我新建了一个 GCP 项目。第一次查询 apps.get 就立即返回了正确的 openUrlTemplate,整个流程第一次就跑通了。事后来看,遇到 Registry 不同步时,新建项目比等待节省的时间不是一星半点。
# 创建新项目
gcloud projects create my-app-drive-prod --name="My App Drive Prod"
# 启用 Drive API
gcloud services enable drive.googleapis.com --project=my-app-drive-prod
# 获取项目编号(用于 apps.get 查询)
gcloud projects describe my-app-drive-prod --format="value(projectNumber)"
在新项目里按正确顺序配置:
- OAuth consent screen → External → 添加测试用户
- Credentials → Create OAuth Web Client → 填写正确的 Redirect URI
- Drive UI Integration → 不勾选自动 OAuth → 填写 Open URL 和 MIME 类型
新项目没有任何历史 Registry 条目,apps.get 保存后立即返回正确值。
更新服务器配置:
GOOGLE_DRIVE_CLIENT_ID=新的 Client ID
GOOGLE_DRIVE_CLIENT_SECRET=新的 Client Secret
重建容器:
docker compose up -d --force-recreate app
排查流程图
出现 redirect_uri_mismatch
↓
F12 → Network → 提取 redirect_uri
↓
是旧地址 / ngrok / 错误域名?
↓ 是
OAuth Playground 获取 token → GET /drive/v3/apps/{appId}
↓
openUrlTemplate 是 OAuth 包装 URL?
↓ 是
Drive UI Integration
→ 取消勾选"Automatically show OAuth 2.0 consent screen"
→ 确认 Open URL 正确 → 保存
↓
每 10 分钟查一次 → apps.get 是否更新?
↓ 否(超过 1 小时) ↓ 是
新建 GCP 项目 完整走卸载重装流程
(立即生效) (断开 → 撤权 → 等 apps.get 更新 → 重新授权)
总结
- 先用 F12 看实际发出的 URL,不要猜,不要看代码
- Drive "Open With" 的 OAuth URL 由 Google 的服务器构造,与你的代码和
.env无关 - 根本原因几乎一定是 "Automatically show OAuth 2.0 consent screen" 被勾选了——这导致错误的
redirect_uri被写入 Registry - 取消勾选是必要条件,不是充分条件:复选框和 Registry 缓存是两个独立问题
- 配置是否生效,唯一可信来源是
GET /drive/v3/apps/{appId},不是 Console 页面 - 取消勾选后 Registry 不一定立即更新,超过 1 小时不更新就直接新建 GCP 项目
- 新项目没有历史缓存,配置保存后立即生效,是最快的修复路径