Git终端里同时管理多个仓库和多个账户,几乎是日常开发里绕不开的一块硬骨头。我最初上手时花了好几个晚上,反复生成密钥、贴公钥、测试连接,最后还是被 Permission denied 卡得死死的;后来才意识到,问题并不是密钥不够多,而是 SSH 签发私钥的方式、Git 提交身份的优先级,这两条线被我搅到了一起。
这篇文章把一个经过验证的多账户配置方案完整写一遍。核心就一句话:传输层的密钥路由归~/.ssh/config管,提交层的用户身份归git config的仓库级配置或includeIf管,两条线各管各的,就不会乱。适合在同一台电脑上切换工作仓库、个人仓库、合作方仓库的开发者,也适合刚打算给电脑配多账户的新手照着做。
1. 先说清楚:多账户配置到底在解决哪两件事
1.1 传输层认证和提交身份根本不是一回事
很多人在配置多账户时容易陷入一个误区,以为只要 SSH 能连上、能推送,提交记录里的作者就是对的。实际上一次 push 要经历两个完全不同的环节:
- 连接远程仓库时,SSH 用私钥证明「你是有权限访问这台服务器的客户端」,这一步决定服务器让不让你读和写。
- 推送上去的 commit 本身带有
user.name和user.email,这一步决定远端仓库在网页端、提交历史里显示的作者是谁。
换句话说,SSH 密钥相当于门禁卡,决定你能不能进入机房;user.name和user.email相当于工牌,决定别人在系统里看到你叫什么名字。门禁卡能开门,不代表工牌上的名字是对的。
我见过不少这样的场景:某位开发者把工作平台的私钥配好了,push 也成功了,结果仓库页面上显示的提交人却是一个老早就废弃的全局用户名,原因就是他在配置密钥的时候完全没管 Git 的提交身份。所以多账户配置的完整内容,一定是「密钥路由 + 身份切换」两件事一起做。
1.2 默认策略只给你一把钥匙,多个账户自然要打架
SSH 客户端默认会按固定文件名找私钥,比如~/.ssh/id_rsa或~/.ssh/id_ed25519。如果你同时使用托管平台 A、公司内网平台、托管平台 B,默认情况下 SSH 会把这一把私钥发给所有目标服务器。
服务器那边拿到公钥之后,会用自己存储的授权公钥列表去比对。你的公钥只注册在平台 A,当它发给平台 B 时,平台 B 自然不认,于是返回Permission denied (publickey)。就算你只用一个平台、但有两个账户,也会出现类似问题,因为平台是靠「公钥」识别你是谁的,默认情况下它只会用同一把钥匙去认门。
所以真正要做的改造是:让 SSH 在面对不同服务器时,主动选择不同的私钥文件。这个能力由~/.ssh/config提供,核心是配置Host别名、HostName真实地址,以及IdentityFile私钥路径。
另外一个容易忽略的点:Git 身份配置同样有优先级。local配置只对当前仓库生效,global配置对当前用户生效,system配置对整台机器生效。优先级是 local 大于 global 大于 system。多个账户场景下,你需要让「工作目录下的仓库」自动使用工作身份,「个人目录下的仓库」自动使用个人身份,这就要用到后面说的includeIf。
2. 密钥分把锁:用~/.ssh/config给不同平台分配不同私钥
2.1 生成命名清晰的多对密钥
既然每一把锁对应一个账户,密钥文件从一开始就要命名清楚,避免以后靠猜。推荐使用ed25519算法生成,相比传统 RSA,它的私钥更短、安全性足够高,而且绝大多数现代平台都支持。
# 生成用于托管平台 A 的密钥 ssh-keygen -t ed25519 -C "a-platform-key" -f ~/.ssh/id_ed25519_a # 生成用于公司内网平台的密钥 ssh-keygen -t ed25519 -C "work-platform-key" -f ~/.ssh/id_ed25519_work命令中的-C只是注释,方便你在平台后台和本地同时识别这把钥匙的用途,不影响功能。执行时如果提示设置 passphrase,我建议设置,但也要有心理准备:没有额外配置的话,每次 SSH 连接都会问你口令。不想频繁输入的,可以在会话里先加载到 ssh-agent,后面细说。
生成完成后,把对应的.pub公钥内容复制到各个平台的 SSH keys 设置页面:
cat ~/.ssh/id_ed25519_a.pub # 复制输出的整行内容这里有个容易踩的坑:复制公钥时不要手抖多复制空格或换行符,有些平台校验得很严格。另外,私钥文件不要发给任何人,也不要传到公开仓库。
2.2 写一份能看懂、能维护的 SSH 配置
~/.ssh/config的作用,是告诉 SSH 客户端「当你连接某个 Host 时,使用哪个私钥、哪个用户名、哪个真实地址」。下面是一份典型的多场景配置:
# 托管平台 A,个人账号 Host a HostName git.example.com User git IdentityFile ~/.ssh/id_ed25519_a IdentitiesOnly yes # 公司内网托管平台,工作账号 Host work HostName git.work.example.com User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes这里最关键的理解是:Host后面跟的名字是「别名」,它不是真实域名,而是你以后在git clone或git remote add时@后面写的那一段。比如你 clone 时写的是git@work:team/project.git,SSH 就会拿work这个别名去匹配配置块,然后真正连接git.work.example.com。
User git是大多数代码托管平台要求的 SSH 用户名,如果你的平台不是这个,需要按平台文档改。IdentitiesOnly yes这一行个人强烈建议加上,它要求 SSH 只使用IdentityFile里明确指定的私钥,不要把 agent 里缓存的其他私钥也一并试探,某些服务端对一次连接里尝试多把钥匙的行为非常敏感,多试几次会直接拒绝。
2.3 用测试命令确认钥匙真的被选中
配置写完后,先用 SSH 的测试命令验证连接,不要直接去 clone 一个大型仓库。
ssh -T git@a ssh -T git@work正常的返回通常是一句问候语,大部分平台会告诉你当前认证的是哪个用户,比如某些平台会返回Hi username! You've successfully authenticated之类的内容。如果看到Permission denied (publickey),按下面的顺序排查:
- 检查
~/.ssh/config里IdentityFile路径有没有写错或者不存在。 - 检查私钥文件权限,
ls -l ~/.ssh/id_ed25519_a确认是否 600;权限过大 SSH 会直接忽略这个私钥。 - 确认公钥确实已经完整粘贴到对应平台的 SSH keys 列表里。
- 加上
-v参数重新测试,ssh -vT git@work,观察输出里有没有出现Offering public key和Server accepts key这类字眼。
我排查过的多数Permission denied都不是密钥本身的问题,而是路径、权限、或者平台后台里公钥没贴全。测试通过后再进行克隆和推送,能把问题隔离在很前面。
3. 提交身份自动切换:includeIf才是多账户的省心解法
3.1 全局身份不能解决身份错乱的问题
假设你已经在global配置里写过一个私人邮箱,比如dev@example.com。后面入职公司、开始往内网平台提交代码时,如果不在仓库里做任何覆盖,所有 commit 都还是带着这个私人邮箱。这在公司内部系统里非常刺眼,别人看到的提交作者完全对不上号。
手动在每个仓库里执行本地配置当然可以:
git config user.name "工作中的昵称" git config user.email "you@company.example"但这要求你记得在每次 clone 或新建仓库后都去设置,漏一次就错一次。对同时维护十几个仓库的人来说,这不是长久之计。
3.2 按目录自动分组,新仓库零配置
Git 的includeIf配置可以在特定条件下加载额外的配置文件,最常见的用法就是按仓库路径来区分身份。思路很简单:把工作仓库统一放在~/work/目录下,个人仓库统一放在~/personal/目录下,然后让 Git 根据仓库所在路径自动加载对应的用户配置。
主配置~/.gitconfig里这样写:
[user] name = 默认用户 email = default@example.com [includeIf "gitdir:~/work/"] path = ~/.gitconfig.work [includeIf "gitdir:~/personal/"] path = ~/.gitconfig.personal然后再创建两个附加配置文件:
~/.gitconfig.work:
[user] name = 工作昵称 email = work-user@company.example~/.gitconfig.personal:
[user] name = 个人昵称 email = personal@example.com平时 git add、commit 时,Git 会先判断当前仓库的.git位置在哪个路径下。如果它在~/work/里面,就自动加载~/.gitconfig.work;如果在~/personal/里面,就加载~/.gitconfig.personal;如果都不在,就用主配置里的默认用户。
路径规则要特别注意:gitdir:~/work/的结尾必须是/,表示「这个目录以及它的所有子目录」。如果你写gitdir:~/work而不带末尾斜杠,含义会变成「目录名精确等于 work」,反而匹配不到子仓库。另外~/会在 Git 读取配置时被展开成当前用户的家目录,所以这份配置可以直接提交到你的个人配置托管仓库或者其他机器上使用。
3.3 includeIf 命中后的优先级问题
一个需要理解清楚的细节是:includeIf引入的配置依然遵守 local 大于 global 的优先级。也就是说,如果某个仓库在很久之前曾经手动执行过git config user.name,那么它优先于includeIf引入的身份,你这边的 includeIf 就不会生效。遇到这种情况,在仓库里用git config --list --local查一下,确认有没有残留的本地覆盖。
我个人的使用习惯是所有仓库严格按路径存放,不在~/work/之外放置工作项目,避免出现路径匹配不到的情况。虽然可以用gitdir/i来忽略大小写匹配,但最可靠的方式还是让目录规划一开始就整齐。
4. 一次完整的实战演练:工作仓库和个人仓库并行跑通
4.1 场景假设
假设你同时有两条开发线:白天维护公司内网托管平台上的工作仓库,晚上维护自己在公开托管平台上的个人仓库。公司内网平台域名假设为git.work.example.com,个人平台假设为code.example.net。你希望在这台电脑上做到:
- 访问工作仓库时,自动使用工作私钥;
- 访问个人仓库时,自动使用个人私钥;
- 提交记录里的身份,分别显示对应的名字和邮箱;
- 以后新建的仓库不用再做任何手动配置。
4.2 从生成密钥到推送,完整命令行流程
第一步,生成两对密钥并确认公钥:
ssh-keygen -t ed25519 -C "work-key" -f ~/.ssh/id_ed25519_work ssh-keygen -t ed25519 -C "personal-key" -f ~/.ssh/id_ed25519_personal cat ~/.ssh/id_ed25519_work.pub # 复制到内网平台后台 cat ~/.ssh/id_ed25519_personal.pub # 复制到个人平台后台第二步,写~/.ssh/config:
Host work HostName git.work.example.com User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes Host personal HostName code.example.net User git IdentityFile ~/.ssh/id_ed25519_personal IdentitiesOnly yes第三步,写~/.gitconfig和两个身份文件,这里不再重复,按上一节的模板即可。
第四步,测试连接:
ssh -T git@work ssh -T git@personal第五步,按路径拉取仓库:
mkdir -p ~/work ~/personal git clone git@work:team/server-project.git ~/work/server-project git clone git@personal:me/toolbox.git ~/personal/toolbox第六步,在仓库内确认身份是否已生效:
cd ~/work/server-project git config user.name # 应该输出“工作昵称” git config user.email # 应该输出 work-user@company.example cd ~/personal/toolbox git config user.name # 应该输出“个人昵称”第七步,正常提交并推送。在改动目录里新增一个测试文件,走一遍 add、commit、push,然后去平台网页端看提交人是否显示正确。如果网页显示错误,多半是同一个仓库里曾经设置过 local 配置,按 3.3 节清理即可。
4.3 验证提交作者信息的小命令
除了去网页看,本地也可以直接用git log检查历史提交的作者信息:
git log --format="%an <%ae>" -5这条命令只输出作者名和邮箱,适合快速核对某个仓库是否已经切换到正确的身份。如果发现历史提交里混入了错误的身份,而且这些提交还未推送到远端,可以修正最近一次提交的作者信息:
git commit --amend --reset-author如果错误提交已经推送出去,那就不要强行改写历史了,否则会破坏协作分支的同步基础;正确做法是后续提交修正身份即可,再和团队说明情况。这也是我反复强调先配置、后提交的原因,改历史永远比改配置麻烦。
5. 高频报错排查与几个让我少走弯路的细节
5.1 一张表看清常见报错
多账户配置过程中,报错信息来来去去就那么几种,把原因理清之后基本都是一次定位。下面这张表是日常排查的真实经验汇总:
| 报错或现象 | 常见原因 | 处理方式 |
|---|---|---|
Permission denied (publickey) | 服务端不认当前私钥,或私钥权限不对 | 用ssh -vT看是哪一步失败;核对平台公钥,检查文件权限 |
Host key verification failed | 首次连接远程主机的指纹未确认,或旧指纹已更换 | 确认指纹可信后选择 yes;主机变化时用ssh-keygen -R 主机名清理旧指纹 |
fatal: Could not read from remote repository | 多是 SSH 认证失败或远端不存在的表象 | 先用ssh -T单独验证连接,再排查仓库路径权限 |
repository not found | 密钥已认证,但没有该仓库的访问权限 | 检查该仓库是否在对应组织/团队下,是否已授权你的账号 |
| 提交记录作者显示错误 | local 配置覆盖了 includeIf | git config --list --local查看并修正残留本地身份 |
| 改动一个仓库,另一个仓库也连不上 | ssh-agent 缓存了旧私钥 | ssh-add -D清空后重新ssh-add指定私钥 |
表格只是索引,下面把几个真正容易踩中的细节展开讲。
5.2 四个容易被忽略但影响很大的细节
第一个是私钥权限。SSH 对私钥文件权限有严格检查,如果~/.ssh/id_ed25519_work权限是 644 或者其他用户可读,SSH 会直接忽略它。遇到认证失败,先ls -l ~/.ssh/看一遍,不是 600 的文件要立刻改:
chmod 600 ~/.ssh/id_ed25519_work ~/.ssh/id_ed25519_personal chmod 700 ~/.ssh第二个是 ssh-agent 的缓存。很多人明明改好了配置,依然发现 SSH 用的还是旧私钥,因为代理进程里还留着之前加载过的钥匙。处理方式很简单:
ssh-add -D # 清空当前会话里的所有私钥 ssh-add ~/.ssh/id_ed25519_work ssh-add ~/.ssh/id_ed25519_personal做完之后再用ssh -T测试,基本能解决「配置是对的可就是连不上」的诡异问题。
第三个是临时排错利器GIT_SSH_COMMAND。当你怀疑 SSH 配置没生效时,可以在 git 命令前临时指定私钥:
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_ed25519_work -o IdentitiesOnly=yes" git fetch origin这样能绕过 config 文件,直接验证某一把钥匙是否能访问远程仓库。如果这样能通而正常方式不通,问题就在 config 配置块本身;如果这样也不通,说明私钥、公钥绑定或平台授权有问题。
第四点比较隐蔽:当你同时配置多个Host块指向同一个HostName时,SSH 会优先匹配第一个,后面的配置块不会生效。同一个平台有两个不同账户时,正确做法是用两个不同别名区分开,而不是写两个 HostName 一样的块去碰运气。这个设计看起来反直觉,但确实是 SSH 的匹配规则。
5.3 这套方法能延伸到哪些场景
这套「密钥路由 + 身份切换」的思路,不只是个人电脑上的多平台账户管理能用。自建 Git 服务器的维护者给多个用户分配部署密钥时,结构完全一样;容器开发环境里,把.ssh/config和.gitconfig通过挂载卷接进去,也能让隔离的容器瞬间拥有和宿主机一样的多账户行为;多台电脑之间同步时,只需要把~/.ssh/config、~/.gitconfig、~/.gitconfig.work、~/.gitconfig.personal这几个文件一并同步,再配上各机器独有的私钥,就能保持统一的行为。
我个人现在最舒服的状态是:新项目一律丢进对应的~/work/或~/personal/目录,然后 clone 完直接提交,完全不用想身份切换这件事。配置好之后,这套机制平时几乎是隐形的,只有在 push 成功、提交作者正确显示的那一刻,你才会意识到当初花时间把两条线理顺是值得的。如果你目前还是一台机器一套 key 一个全局身份的配置,建议先用几天的临时切换来验证这个方案,稳了之后再逐步迁移旧仓库。