跳到主要内容

Gitea 冷推送之谜:第一次 push 必失败,第二次就好了

发布于Git开发环境

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 加一串乱码现出原形时,谜底已经写在里面。