GitHub OAuth 端点无 CORS 与纯前端取 Token (GitHub OAuth CORS Gap & Browser-only Token Acquisition)
一句话结论
api.github.com 开了跨域(Cross-Origin Resource Sharing, CORS),所以带着 token 从浏览器直接调 GitHub API 是可行的;但 github.com/login/* 的 OAuth 与设备流(Device Flow)端点没有开 CORS,所以在纯前端完成登录、把授权码换成 token 这一步做不到,必须有一个后端或代理。
发生条件
任何"纯前端应用想接 GitHub 登录"的场景:静态 SPA、浏览器插件、本地跑的纯前端工具、无后端 WebContainers 应用。
症状
浏览器 fetch 到 https://github.com/login/oauth/access_token 或 https://github.com/login/device/code,控制台报 CORS 错误,响应被浏览器拦下,Network 面板里该请求标红且读不到 body。注意:请求本身在服务端是成功的,是浏览器在拿到响应之后把它丢弃。
容易误判的点:用 curl 测同一个 URL 会正常返回,于是以为"接口没问题,是我代码写错了"。curl 不受同源策略约束,所以这不是有效判据。
实测证据(2026-09-21,走本地代理 7890 实测)
在请求里主动加 Origin: https://example.com,看响应头有没有 Access-Control-Allow-Origin:
| 端点 | Access-Control-Allow-Origin | 前端可直调 |
|---|---|---|
api.github.com(REST) | *,预检还回 access-control-allow-headers: Authorization, ... | 可以 |
github.com/login/oauth/access_token | 无 | 不可以 |
github.com/login/device/code | 无 | 不可以 |
api.github.com/graphql(匿名) | — | 401/403,GraphQL 必须带 token |
oauth2.googleapis.com/token(对照组) | 回显 Origin,即有 CORS | 可以 |
官方旁证:@octokit/oauth-methods 的 README(GitHub 自家维护)明确写 createDeviceCode()、exchangeDeviceCode()、exchangeWebFlowCode()、refreshToken() 在浏览器中因缺少 CORS 头会失败。GitHub 官方文档在 Web Application Flow 一节也写 "CORS pre-flight requests (OPTIONS) are not supported at this time"。
尝试过但不成立的路
- 换设备流(Device Flow)绕开:设备流不需要
client_secret,直觉上应该能纯前端跑,但 GitHub 同样没给这两个端点开 CORS,仍然失败。设备流是为 CLI、电视这类无浏览器环境设计的,那些环境不受同源策略约束。 - 用 PKCE 走公开客户端:PKCE 解决的是"不暴露 secret",解决不了"响应头里没有 CORS",那条换 token 的请求照样被拦。
- 自己写 CORS 代理:技术上可行,但 token 会流经第三方;且依赖外部免费代理随时会挂,不适合开源分发。
根因
GitHub 只给数据接口(api.github.com)配了 CORS 中间件,没给认证端点(github.com/login/*)配。这是 GitHub 的实现选择,不是协议要求——对照组的 Google token 端点就开了。所以"纯前端登录"在 Google 那边做得到,在 GitHub 这边做不到,差别在服务端配置而不在客户端代码。
可用的解法
方案 A:有后端时走标准 OAuth Web Flow(多用户产品用这个)
前端点"用 GitHub 登录" → 跳 github.com/login/oauth/authorize → 带 code 跳回 → 由后端用 client_secret 换 token。后端只需要这一个接口,交换完成后建议把 token 留在后端、给浏览器发自己的 session cookie,让 GitHub token 不落到前端。
方案 B:纯前端本地工具用 PAT(无后端时用这个)
没有后端、且应用跑在用户自己机器上时,让用户自建一个 Personal Access Token(个人访问令牌, PAT)粘贴进来,前端存 localStorage,之后直接打 api.github.com 即可。这一路完全不涉及 CORS 问题,因为 api.github.com 本来就是开放的。
体验可以做好:给一个预填权限的直达链接,用户点开只需生成再复制。
https://github.com/settings/tokens/new?scopes=repo&description=<应用名>建议用 classic token 勾 repo 一条;fine-grained token 要用户逐个勾仓库,摩擦太大。
小技巧:X-OAuth-Scopes 在 Access-Control-Expose-Headers 里,前端读得到,可以回显给用户看"你这个 token 能干什么、不能干什么",降低粘贴凭据的心理门槛。
方案 C:匿名调用(零配置兜底)
不带任何 token 直接请求 api.github.com 是允许的,CORS 也放行,但:
- 限额只有 60 次/小时(实测
x-ratelimit-limit: 60),按 IP 计。agent 类应用翻几个仓库就耗尽。 - 只能读公开数据,写操作与私有仓库都不行。
- GraphQL 端点匿名直接 403。
适合当"不配置也能玩两下"的默认档,不适合当唯一路径。
下次怎么快速识别
想给某家服务做纯前端登录时,先看该服务 token 端点有没有 CORS,再看它文档有没有提 browser / SPA / CORS。判据是服务端配置,跟客户端怎么写无关。
一行验证命令(加 Origin 头看有没有回 Access-Control-Allow-Origin):
curl -s -D - -o /dev/null -X POST https://<token-endpoint> \
-H "Origin: https://example.com" -d "x=1" | grep -i access-control安全边界
方案 B 的前提是应用跑在用户自己机器上。同一份代码如果部署到公网域名上,就变成了"让访客把凭据粘进你的页面",性质完全不同,那时必须改用方案 A。开源分发时要在 README 里写清这一点。
相关
- CORS 带凭据与通配符互斥 (CORS Credentials vs Wildcard)
- CSP (Content Security Policy 内容安全策略)
- Web 登录鉴权 (Session, Cookie, Bearer, JWT)
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web