git push 403?先查 Windows 上那把陈旧凭据
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. 为什么命令里带了令牌,还是用了旧凭据
三个原因叠在一起:
- helper 优先。流程是「按 helper 列表逐个 fill,拿到可用的用户名 + 口令就用」;只返回用户名不算拿到,Git 会继续问下一个;
- helper 发的是 Basic 认证。它把
用户名:口令做 base64 塞进Authorization: Basic ...(base64 只是编码,不是加密)。旧口令过期或被吊销,服务端一般回 401;有些平台对「凭据有效但无该操作权限」回 403,具体以平台文档为准。两种在客户端看起来都像「没权限」,所以要先确认凭据来源,再讨论权限; - 临时 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. 检查清单
git config --show-origin --get-all credential.helper加git config --get-regexp '^credential\.':确认 helper 是谁、配置在哪一层、有没有 URL 专属项;GIT_TRACE=1 GIT_TERMINAL_PROMPT=0 git push:看run_command行确认调用的是哪个 helper(它只证明调用,不证明通过);git config --get-all http.extraHeader:有没有残留的临时认证头;cmdkey /list | findstr /i "git:"(cmd/PowerShell):有没有年代久远的旧条目;- 有就删(第 4 节 A),用
cmdkey /list复核;旧令牌仍有效则到平台吊销并轮换; - 仍失败,用
-c credential.helper=加临时 header 复现,确认是否 helper 干扰(用完unset); - 认证通了还失败,依次排除非快进、hook、配额、超时,再查 scope、组织角色、分支保护;
- 最后用
ls-remote对比远端 sha,并配合 trace 判断认证,而不是只看它是否成功。
这件事的教训不在「令牌权限」,而在「认证链路里可能不止一把凭据」。遇到 403,先证明 Git 送出去的是谁,通常比翻权限文档快得多。
- 点赞
- 收藏
- 关注作者
评论(0)