git push 403?先查 Windows 上那把陈旧凭据

举报
岚枫 发表于 2026/10/26 09:30:30 2026/10/26
【摘要】 PAT 用 API 验证读写都正常,git push 却一直 403。问题在 Windows 凭据管理器里一条很旧的凭据:GCM 先把旧口令送了出去。附 GIT_TRACE 定位、cmdkey 清理、单次绕开 helper 的写法与 sha 验证。适用于 Windows + GCM + PAT。

git push 403?先查 Windows 上那把陈旧凭据

给仓库推提交时 git push 返回 403,第一反应是令牌权限不够。但用同一个 PAT(Personal Access Token,个人访问令牌)去调平台的用户信息和仓库接口,读写都正常;账号在组织里的角色、仓库可见性、分支保护规则也逐一确认过。最后发现问题不在令牌本身,而在 Windows 的凭据管理器里躺着一条很旧的凭据——Git 每次推代码,送出去的都是它,而不是手里这把新令牌。

本文适用于 Windows + Git Credential Manager(GCM)+ PAT 的组合,只做凭据来源排查,不涉及绕过 SSO、分支保护或组织策略;确实受组织策略限制时,走管理员流程。以下命令默认在 Git Bash 里执行,标注 PowerShell 的除外;示例中的 origin、main 请按 git remote -v、git branch --show-current 的实际情况替换。

1. 先别改权限,先确认凭据是谁给的

Git 的认证不是「命令行里带了令牌就用令牌」。需要认证时,Git 会按 credential helper 配置逐个询问,Windows 上通常是 GCM;helper 里存着旧凭据就直接返回,命令里临时给的认证信息不一定有机会生效。先把这一步照出来:

GIT_TRACE=1 GIT_TERMINAL_PROMPT=0 git push origin main 2>&1 | grep -iE "run_command|credential|http"

PowerShell 里的等价写法(用完把环境变量删掉,免得影响后续命令):

$env:GIT_TRACE=1; $env:GIT_TERMINAL_PROMPT=0
git push origin main 2>&1 | Select-String -Pattern "run_command|credential|http"
Remove-Item Env:GIT_TRACE, Env:GIT_TERMINAL_PROMPT

要点:

  • GIT_TERMINAL_PROMPT=0 让它在需要交互输入时直接失败,避免卡在那里等你输密码;
  • GIT_TRACE 只说明「调了谁」:能看到类似 run_command: 'git-credential-manager.exe get' 的命令行。出现这一行,说明凭据来自 helper,不是来自你的命令;但它证明不了凭据被接受——认证是否真正通过,要看 push 有没有走到上传阶段、服务端还有没有回 401;
  • GIT_TRACE 不打印用户名、口令和 Authorization 头。想看 HTTP 报文要加 GIT_TRACE_CURL=1,Git 默认把 Authorization 标成 <redacted>;不同版本和第三方 helper 输出不完全一致,贴日志前自己过一眼;
  • 特例:如果曾把凭据直接写进 remote URL(https://user:token@host/...),GIT_TRACE 反而会把它原样打印出来。排障前先 git remote -v 看一眼 URL 里有没有内嵌凭据,有就先改掉——并且那把令牌已经可能在日志、脚本或仓库历史里留下过痕迹,应当吊销轮换。

再确认 helper 从哪儿来,以及有没有残留的临时认证头:

git config --show-origin --get-all credential.helper
git config --get-regexp '^credential\.'
git config --get-all http.extraHeader

credential.helper 是多值配置:system → global → local → 命令行 -c 依次累加,后配的不会覆盖前面的,Git 按顺序逐个询问,先给出可用凭据的生效。--show-origin 会告诉你每一行写在哪一层。第二条命令用来查 credential.<url>.helper 这类带域名的配置——只查 credential.helper 会漏掉它们,而后面 -c credential.helper= 也清不掉这种 URL 专属项。

2. 凭据到底存在哪

GCM 默认把凭据存在 Windows 凭据管理器(credentialStore=wincred)。先确认存储方式:

git config --get credential.credentialStore

输出为空或为 wincred 时,用下面的命令查看目标名(在 cmd 或 PowerShell 中执行;Git Bash 会把 /list 当路径转换掉):

cmdkey /list | findstr /i "git:"

目标名各版本显示略有差异,常见形式是 Target: LegacyGeneric:target=git:https://<host>,有的版本只显示 git:https://<host>,以你本机输出为准。如果 credentialStore 是 dpapi 或其他值,cmdkey 里根本看不到,这时改用第 4 节的 git credential reject,或按 GCM 文档清理对应存储。

图形界面在「控制面板 → 用户账户 → 凭据管理器 → Windows 凭据」,看「普通凭据」里以 git: 开头的那几条;界面里能看到修改日期,cmdkey 不显示。对应你已经换过令牌的主机,基本就是它。

3. 为什么命令里带了令牌,还是用了旧凭据

三个原因叠在一起:

  1. helper 优先。流程是「按 helper 列表逐个 fill,拿到可用的用户名 + 口令就用」;只返回用户名不算拿到,Git 会继续问下一个;
  2. helper 发的是 Basic 认证。它把 用户名:口令 做 base64 塞进 Authorization: Basic ...(base64 只是编码,不是加密)。旧口令过期或被吊销,服务端一般回 401;有些平台对「凭据有效但无该操作权限」回 403,具体以平台文档为准。两种在客户端看起来都像「没权限」,所以要先确认凭据来源,再讨论权限;
  3. 临时 header 和 helper 不是二选一,谁先生效取决于服务端回没回 401。认证流程是「先发请求 → 401 才去问 helper」:http.extraHeader 带的 Authorization 如果有效,服务端不会回 401,helper 根本不会被调用;无效或缺失时,才轮到 helper 出手。而 403 通常不触发「换一套凭据重试」,所以到底有没有 helper 参与,只能靠上面的 trace 证明,不能靠「我加了 header」推断。最后以本机 trace 和服务端行为为准。两者同时存在时,多数版本会让自定义头生效、遮蔽 helper 给的那份;个别版本或认证协商场景下也可能出现两个 Authorization 头。

所以「换把令牌再试」经常无效——换的是手里的令牌,送出去的还是 helper 里那条。

4. 三种改法,按推荐程度排序

A. 删掉陈旧凭据(首选)。一般不需要管理员权限。目标名的写法随版本不同,两种都试一下,以「能删掉、且列表里确实消失」为准:

# 简名
cmdkey /delete:"git:https://<host>"
# 若提示找不到,再用列表里 Target 显示的完整名
cmdkey /delete:"LegacyGeneric:target=git:https://<host>"
  • 删完用 cmdkey /list | findstr /i "git:" 复核;删除成功一般返回 0、找不到目标返回非 0,但各版本提示不一,以列表为准;
  • 也可以不依赖 cmdkey 的命名,让 Git 调 helper 的 erase:printf 'protocol=https\nhost=<host>\n\n' | git credential reject;GCM 的等价写法是 printf 'protocol=https\nhost=<host>\n\n' | git-credential-manager erase;
  • 再推一次,helper 找不到存货就会重新发起认证。这个主机下的所有仓库都会重新认证一次,同机多账号协作时别弄混;
  • 删除本地条目 ≠ 吊销凭据:那条旧令牌如果本身还有效,应到平台吊销并换新,同时看一眼最近的推送记录有没有异常;
  • 长期使用的话,改用 SSH 密钥,或用 GCM 的浏览器交互登录,可以少一类问题。

B. 单次命令绕开 helper。适合临时机器或排障验证,不要写进脚本、不要放进仓库。下面按 Git Bash 写,Windows 路径用 C:/Users/<你>/token.txt;MSYS 的 chmod 600 在 NTFS 上不保证 ACL,建议先用 icacls "C:\Users\<你>\token.txt" /inheritance:r /grant:r "$USERNAME:R" 收紧权限,用完删掉这个文件:

GIT_USER=oauth2   # 用户名占位,因平台而异,见下方说明
PAT=$(head -n 1 "C:/Users/<你>/token.txt" | tr -d '\r\n')
[ -n "$PAT" ] || { echo "token 文件第一行是空的"; exit 1; }
AUTH=$(printf '%s:%s' "$GIT_USER" "$PAT" | base64 -w0)

GIT_TERMINAL_PROMPT=0 git \
  -c credential.helper= \
  -c http.extraHeader="Authorization: Basic $AUTH" \
  push origin main

unset PAT AUTH
rm -f "C:/Users/<你>/token.txt"   # 用完即删

PowerShell 等价写法:

$env:GIT_TERMINAL_PROMPT=0
$PAT  = (Get-Content "C:\Users\<你>\token.txt" -TotalCount 1).Trim()
$AUTH = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("oauth2:$PAT"))
git -c credential.helper= -c http.extraHeader="Authorization: Basic $AUTH" push origin main
Remove-Variable PAT, AUTH -ErrorAction SilentlyContinue
Remove-Item Env:GIT_TERMINAL_PROMPT -ErrorAction SilentlyContinue
  • 别用 ^[A-Za-z0-9]{20,}$ 之类的正则去「过滤出令牌」:ghp_、glpat-、github_pat_ 这些前缀含 _ 和 -,会被整行丢掉;也别用 tail -1,文件末尾的空行会让它取到空字符串。取第一行、去掉 CRLF、检查非空就够;
  • -c credential.helper=(空值)清空的是 credential.helper 列表;第 1 节 get-regexp 查到的 credential.<url>.helper 不在内,要单独 --unset-all;
  • 只加 header 不清 helper 时,可能同时发出两套认证,实际以哪个为准取决于版本与服务端,别拿它当长期方案;
  • 令牌会出现在进程命令行里,同机其他用户用进程列表就能看到,所以只在本机、只在必要时用;
  • 用户名占位因平台而异:oauth2 是常见约定,也有平台要求填 token 名、真实用户名或「租户名/用户名」,以平台文档为准。

C. 指定 GCM 对该主机使用 generic 认证。这不是「避免旧条目被复用」,而是换一条认证路径来确认问题:

git config --global credential.https://<host>.provider generic
git config --global --get-regexp '^credential\.'
# 排障结束,还原
git config --global --unset credential.https://<host>.provider

generic 会退化为标准 Basic 认证,只适合排障,不建议长期使用;在启用 SSO、条件访问或强制组织登录的环境里,这样做可能触犯组织策略,动手前先确认允许。它也不会自动删掉已有条目,改完仍要先按 A 清一次,否则旧存货照样优先。

5. 验证:不要只看 exit code

git rev-parse HEAD
git ls-remote origin refs/heads/main

两行 sha 一致才算推送完成。但 ls-remote 只证明你有读权限,公开仓库甚至匿名就能列出来;它通了不代表写凭据被接受。判断认证是否通过,要看 trace 里 helper 之后 push 有没有走到上传阶段、服务端是否还回 401,再用 cmdkey /list 确认旧条目确实删掉或更新了。

认证确认没问题、push 仍被拒时,先排除非认证原因:非快进(错误里带 rejected/non-fast-forward,先 git pull --rebase)、服务端 hook 拒绝(信息里通常写明原因)、仓库配额或单文件大小限制(常见 413)、网络超时。这些排除掉,再查令牌 scope、组织角色和分支保护才有意义。

6. 检查清单

  1. git config --show-origin --get-all credential.helper 加 git config --get-regexp '^credential\.':确认 helper 是谁、配置在哪一层、有没有 URL 专属项;
  2. GIT_TRACE=1 GIT_TERMINAL_PROMPT=0 git push:看 run_command 行确认调用的是哪个 helper(它只证明调用,不证明通过);
  3. git config --get-all http.extraHeader:有没有残留的临时认证头;
  4. cmdkey /list | findstr /i "git:"(cmd/PowerShell):有没有年代久远的旧条目;
  5. 有就删(第 4 节 A),用 cmdkey /list 复核;旧令牌仍有效则到平台吊销并轮换;
  6. 仍失败,用 -c credential.helper= 加临时 header 复现,确认是否 helper 干扰(用完 unset);
  7. 认证通了还失败,依次排除非快进、hook、配额、超时,再查 scope、组织角色、分支保护;
  8. 最后用 ls-remote 对比远端 sha,并配合 trace 判断认证,而不是只看它是否成功。

这件事的教训不在「令牌权限」,而在「认证链路里可能不止一把凭据」。遇到 403,先证明 Git 送出去的是谁,通常比翻权限文档快得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。