Gitea 冷推送之谜:第一次 push 必失败,第二次就好了
HTTPS 推送 Gitea,闲置一小时后第一次 push 必报 Authentication failed,重试又成功——真相是凭证一小时就过期,而 GCM 不会主动刷新。附排查过程与 PAT 修复方案。
症状
自建 Gitea,HTTPS 推代码:一小时没 push 过,再 push 第一次必然失败:
remote: Failed to authenticate user
fatal: Authentication failed for 'https://git.example.com/my-team/my-project.git/'
紧接着再 push 一次,成功。SSH 完全正常,只有 HTTPS 中招。
排查
先缩小范围:同一仓库 SSH 推送一直正常,只有 HTTPS 失败——网络、DNS、Gitea 本身都没问题,嫌疑锁定在 HTTPS 的认证环节。
再用 curl 验证报错的性质——直接请求 git 的 HTTP 端点,不带凭证返回 401 Unauthorized,故意带错误凭证返回 401 Failed to authenticate user,和 push 报错一字不差。Gitea 确实是把送上去的凭证当成了无效凭证,不是网络层抽风。
但凭证不该是“错的”——错凭证会次次失败,不会重试就好。它总是在闲置约一小时后才失败:这不像凭证错误,更像凭证过期。
那就看 git 实际用的凭证是什么。凭证由 Git Credential Manager(GCM)管理——Windows 版 Git 自带的凭证助手。打开 控制面板 → 用户账户 → 凭据管理器 → Windows 凭据,git:https://git.example.com 条目里:用户名是 OAUTH_USER,密码是一串 869 字符的乱码——不是账号密码,是 GCM 首次认证时自动走 OAuth 流程拿回的 access token,被当作密码存了下来。解码一看,有效期正好 3600 秒——和“闲置约一小时就失败”的规律完全对上了。
真相:凭证会过期,GCM 却不主动刷新
问题从来不在凭证“是什么”,而在两点:它 1 小时就过期(Gitea OAuth 的默认有效期),而 GCM 过期后不主动刷新——明明存着 refresh token,却不在交出凭证前检查有效期,非要被服务器打回一次才动手。这是 GCM 的已知缺陷,不是配置问题:
平时 push:GCM 返回未过期 token → 验签通过 → 成功
冷 push(token 已过期,距换发超过 1h):
GCM 把【已过期】的 token 原样交出
→ Gitea 校验 exp 已过期 → 401 → fatal
→ git 通知 GCM 凭证被拒,GCM 清掉旧 token
第二次 push:
存储已空 → GCM 用留存的 refresh token 静默换新(或重新走授权)→ 成功
SSH 走公钥认证不经过 OAuth,所以免疫。
修复
方案一:改用 PAT(治本,推荐)
既然根源是“凭证会过期 + GCM 不主动刷新”,那就让“过期”这个前提消失——换用可永不过期的个人访问令牌(PAT)。
Gitea 网页 设置 → 应用 → 生成令牌:权限只给 repository 读写,其余全部无访问权限;过期时间选永不过期。
然后清掉旧 OAuth 凭证:凭据管理器里删掉 git:https://git.example.com 条目。下次 push 若弹出 OAuth 授权页面,直接关掉,GCM 会降级为终端输入:
- 用户名填 Gitea 平台用户名
- 密码填 PAT 值
输入一次后 GCM 会存下 PAT——一小时的轮回就此终结。
方案二:服务端调大 token 有效期(治标,不推荐)
# Gitea app.ini
[oauth2]
ACCESS_TOKEN_EXPIRATION_TIME = 86400
重启后新 token 有效 24 小时,但这只是把“每小时必败一次”拖成“每天必败一次”:GCM 不主动刷新的 bug 原封不动,每天上班第一次 push 照样撞墙;还得动服务器,泄露风险窗口也变大。除非确实需要 OAuth 流程(如统一账号体系),否则没有理由选它。
番外:GitHub 也换成 PAT
GCM 对 GitHub 存的是长期 token,没有一小时过期的毛病;换 PAT 是为了摆脱 OAuth 凭证、统一管理。PAT 分 classic 和 fine-grained 两种:
- fine-grained(官方主推):仓库级 + 权限级双重限定,粒度细。但一个 token 只作用于一个 resource owner——owner 选自己就只管自己的仓库,要访问加入的组织仓库得按组织各建一个 token(组织还得允许 fine-grained PAT,有的还要管理员批准)
- classic:scope 粗,但一个 token 覆盖全账号——个人仓库 + 所有你有权限的组织仓库一起管
要同时推个人仓库和组织仓库,classic 一个 token 就够,推荐配置:
路径:Settings → Developer settings → Personal access tokens → Tokens (classic)
Expiration: No expiration
Select scopes:
☑ repo ← 公有/私有仓库完整读写,push/pull 全靠它
☑ workflow ← 唯一例外区:.github/workflows/ 目录
其余全部不勾(delete_repo、admin:* 等高危项尤其别碰)
workflow 是 classic 里同样的例外区:只勾 repo 的话普通 push 没事,可一旦提交里改了 .github/workflows/ 文件,push 就会被拒。
生成后 token 以 ghp_ 开头,只显示一次,立刻复制。换凭证和 Gitea 同款:凭据管理器里删掉 git:https://github.com 条目,下次 clone / push / pull 时按提示输入——用户名填 GitHub 用户名,密码填 token,GCM 存下来以后就不再问了。
只在个人仓库用、且想要更小权限面的话,再考虑 fine-grained:Contents + Workflows 两项读写即可,其余权限一律不选。
结论
整个谜题归结为一句话:凭证 1 小时就过期,而 GCM 不主动刷新。
值得带走的方法论也只有一条:遇到“间歇性认证失败、重试即好”,别急着怀疑网络——先打开凭据管理器,看 git 实际用的凭证长什么样。OAUTH_USER 加一串乱码现出原形时,谜底已经写在里面。