一句话结论

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 里写清这一点。

相关


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web