你在生产环境点 Google / GitHub 登录,授权完成后浏览器跳回的是 http://localhost:3000/auth/callback(你看到的是空白页),或者跳到一个完全不相关的域,又或者 OAuth provider 直接报 “redirect_uri_mismatch”。这是 OAuth 流程里最常见的配置坑:你代码里发给 provider 的 redirect_uri 跟 provider 后台允许列表里的那串字符不完全相等。
OAuth provider(Google、Auth0、Supabase、Clerk)的 redirect_uri 校验是逐字符精确匹配:协议(http vs https)、域名、端口、路径、尾部斜杠,任何一处不一致都会被拒。理解这点,问题排查就有了准星。
常见原因
按命中率从高到低:
1. 生产 callback URL 不在允许列表
最高频。你在 dev 测试时加了 http://localhost:3000/auth/callback,部署上生产后忘了把 https://yourdomain.com/auth/callback 也加进去。
如何判断:provider 错误页通常显示”redirect_uri_mismatch”和它收到的 URI;对照 provider 后台的 allowed list 看是否包含。
2. 代码里硬编码了 localhost
const redirectUri = "http://localhost:3000/auth/callback"; // ❌ 永远 dev URL
部署后这行没变,请求里 redirect_uri= 还是 localhost,provider 拒收。
如何判断:搜 codebase 是否硬编码 localhost 或固定域名。
3. 协议或尾斜杠不匹配
https://example.com/cb 和 https://example.com/cb/(多了一个斜杠)被认为不同。http:// vs https:// 也是。
如何判断:浏览器 Network 面板看请求里的 redirect_uri=,跟 provider 后台逐字符对。
4. 用了 www. vs 裸域
https://www.example.com/cb 和 https://example.com/cb 不同。如果你站点同时支持两个但只在允许列表里加了一个,从另一个发起就失败。
如何判断:你的站点是否有 www → 非 www 重定向(或反过来)?
5. preview / staging 环境的 URL 没加
Vercel preview deployment 每次都是 xxx-yyy.vercel.app 这种随机域,OAuth 在 preview 环境必然失败,除非你专门处理。
如何判断:是否在 preview deployment 触发?
6. Provider 自动 strip 路径或 query
某些 provider(特别是 SAML / 老旧 SSO)会忽略 path,只匹配 origin。你在 redirect_uri 加了 ?from=signup query,被剥掉后跳到 root。
如何判断:跳回的 URL 是 / 而不是你写的 /auth/callback。
最短修复路径
Step 1:抓 provider 收到的实际 redirect_uri
点登录按钮,浏览器 Network 面板看发给 provider 的请求:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=...&
redirect_uri=https%3A%2F%2Fyourdomain.com%2Fauth%2Fcallback&
...
把 redirect_uri= 的值复制下来(URL-decode 一下),这就是 provider 收到的字符串。
Step 2:在 provider 后台把它加入白名单
把上一步复制的字符串原样贴到 provider 的 Allowed Redirect URIs:
Google Cloud Console:
APIs & Services → Credentials → OAuth client → Authorized redirect URIs
Auth0:
Application → Allowed Callback URLs
Supabase:
Authentication → URL Configuration → Site URL + Redirect URLs
Clerk:
Paths → Settings
加完保存。Google 的设置生效需要 5-10 分钟(其他 provider 即时)。
Step 3:代码里改用 env 驱动 base URL
// Next.js 示例
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL; // 永远不要 fallback 到 localhost
if (!baseUrl) throw new Error("NEXT_PUBLIC_SITE_URL not set");
const redirectUri = `${baseUrl}/auth/callback`;
然后在 host 平台(Vercel / Netlify / Railway)按环境配置:
Local (.env.local): NEXT_PUBLIC_SITE_URL=http://localhost:3000
Preview: NEXT_PUBLIC_SITE_URL=(动态,见 Step 4)
Production: NEXT_PUBLIC_SITE_URL=https://yourdomain.com
Step 4:处理 preview deployment
Vercel 每个 preview 是不同子域。两个方案:
方案 A(简单但宽松):
在 provider 加 https://*.vercel.app 通配(如果 provider 支持)
方案 B(推荐,安全):
preview 用 process.env.VERCEL_URL 拼:
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: process.env.NEXT_PUBLIC_SITE_URL;
然后在 provider 加固定的 https://your-app-git-*-team.vercel.app
或者只用一个固定的 preview alias(推荐)
Step 5:端到端验证
无痕窗口跑:
1. 访问生产 URL
2. 点登录
3. Network 面板看请求里 redirect_uri 是不是对的
4. 走完 Google 授权
5. 跳回页面,看 cookie / session 是否落上
6. 刷新一次,看登录态是否保持
任何一步失败,看 console / network 错误信息。
Step 6:协议 / 尾斜杠精确对齐
如果还失败,把 provider 后台和代码里的 redirect_uri 各打印一行,diff 一下:
echo "provider: https://example.com/auth/callback" # 后台里的
echo "code: https://example.com/auth/callback/" # 代码里的(多个 /)
diff <(echo ...) <(echo ...)
差一个字符都不行。
预防建议
- 允许列表跟 deploy workflow 一起维护:新增环境时加 redirect URI 是 deploy checklist 一项
- 代码里禁止硬编码 localhost / 固定域名,CI 加 lint 规则扫
- env 命名一致:dev / preview / production 都用 NEXT_PUBLIC_SITE_URL,行为统一
- preview 环境提前规划:要么白名单通配,要么固定 alias
- 上线前在生产域跑一次完整登录流程,不要只本地测
- provider 后台允许列表加注释(哪个环境用、谁加的、什么时候),方便后续审计
- 定期 review 允许列表,删除已下线的 dev / staging URL(残留的旧 URL 是安全隐患)