Git Credentials 原理
提出问题
Git 支持三种远程访问协议,分别是:SSH、HTTP/HTTPS 以及 git 协议。国内的 GitHub 用户通常会选择使用 HTTPS 协议,主要原因是该协议可以通过 HTTP 代理服务器来访问 GitHub。
使用 HTTPS 协议的不便之处在于每次请求都需要提供用户名和密码。因此,Git 通过一个用户凭证管理系统来解决这个问题,这个系统称作 Git Credentials。通过这个系统,Git 可以实现存储和查询用户凭证。
对普通用户来说,无论你是使用命令行,还是 Sourcetree 或 TortoiseGit 之类的 GUI 工具,只要在 Git 弹出的对话框里输入用户名和密码(Personal Access Token)就可以完成身份验证了。Git 会自动记住账号,下次便无需再次输入(在某些配置环境下,Git 不会记住密码,需要每次手动输入用户名密码)。这一切看起来都简单得理所当然。
不过,一旦当我们需要同时管理多个属于不同 GitHub 账号的 repo 时,情况就变得复杂起来。某些情况下,Git 会使用之前记住的用户凭证来登录不同的 GitHub 账号。这样自然也就无法完成身份验证了。
那么,如何才能做到不同的 repo 使用不同的 GitHub 账号呢?在解答这个问题之前,首先要了解 Git 是如何管理用户凭证的。
凭证存储
Git credentials 支持不同的 backend,以实现不同的凭证存储方式。这些 backend 被称作 credential helper,中文姑且翻译为助手。通过 git config credential.helper
命令可以查询当前使用的助手。Git credentials 常见的助手有:
- cache
将用户凭证存储在内存中,而不保存到磁盘,并且可以设置过期时间。 - store
将用户凭证以明文的形式存储在文件中。默认保存在~/.git-credentials
文件。 - wincred
将用户凭证存储到 Windows 的凭据管理器中。 - osxkeychain
类似于 wincred。将用户凭证存储到 macOS 当前用户的钥匙串中。 - manager-core
由第三方工具 Git Credential Manager 提供的 credential helper。该工具前身是 Git Credential Manager for Windows,仅支持 Windows 平台。现在的 GCM 已经支持跨平台。
设置
为当前仓库指定 credential helper:
powershellgit config credential.helper store
等同于在 repo 的 .git/config
中配置:
ini[credential]
help = store
还可以为助手指定参数:
powershellgit config credential.helper 'store --file ~/.my-credentials'
等同于在 repo 的 .git/config
中配置:
ini[credential]
help = store --file ~/.my-credentials
定义凭证使用的用户名:
powershellgit config credential.username fournoas
等同于在 repo 的 .git/config
中配置:
ini[credential]
username = fournoas
此外,可以为凭证指定一个由 URL 定义的上下文。只有当请求 URL 匹配这个上下文,才会使用此凭证。
powershellgit config credential.https://github.com.helper wincred
等同于在 repo 的 .git/config
中配置:
ini[credential "https://github.com"]
help = wincred
请求凭证
Git 通过 git credential
命令来访问凭证。
usage: git credential (fill|approve|reject)
该命令接受三个参数:
fill
读取符合条件的凭证approve
写入凭证reject
清除符合条件的凭证
此处假设我们需要读取 GitHub 上用户名为 fournoas 的用户凭证。运行命令 git credential fill
,这是一个交互式的命令,运行后会等待用户输入筛选条件,每行输入定义一个条件,格式为 key=value
,输入空行提交查询。支持的查询关键字有:
- protocol
- host
- path
- username
- url
下面是一个查询成功返回的样例:
protocol=https
host=github.com
username=fournoas
protocol=https
host=github.com
username=fournoas
password=xxxxxxxxxxxx
空行前是查询参数,空行后是返回的查询结果。如果助手中储存了多条符合条件的记录,也仅返回一条。
自定义助手
我们可以通过以下命令直接访问助手:
git credential-cache
git credential-store
git credential-wincred
git credential-manager-core
之前配置的 credential.helper
最终也是执行相应的命令。下表展示了配置所支持的值和对应的行为:
配置值 | 行为 |
---|---|
foo | 执行 git-credential-foo |
foo -a --opt=bcd | 执行 git-credential-foo -a --opt=bcd |
/absolute/path/foo -xyz | 执行 /absolute/path/foo -xyz |
!f() { echo "password=s3cre7"; }; f | ! 后面的代码会在 shell 执行 |
按照此原理,我们也可以自己实现自定义的助手命令。
助手命令需要接受一个 action 参数,通常格式为 git-credential-foo [options] <action>
。
get
从助手中获取符合条件的凭证;store
将凭证保存到助手中;erase
将符合条件的凭证从助手中清除。
这三个 action 和 git credential
的三个参数相对应,接口格式也一致。下面是官方文档给出的一个 Ruby 实现:
ruby#!/usr/bin/env ruby
require 'optparse'
path = File.expand_path '~/.git-credentials'
OptionParser.new do |opts|
opts.banner = 'USAGE: git-credential-read-only [options] <action>'
opts.on('-f', '--file PATH', 'Specify path for backing store') do |argpath|
path = File.expand_path argpath
end
end.parse!
exit(0) unless ARGV[0].downcase == 'get'
exit(0) unless File.exists? path
known = {}
while line = STDIN.gets
break if line.strip == ''
k,v = line.strip.split '=', 2
known[k] = v
end
File.readlines(path).each do |fileline|
prot,user,pass,host = fileline.scan(/^(.*?):\/\/(.*?):(.*?)@(.*)$/).first
if prot == known['protocol'] and host == known['host'] and user == known['username'] then
puts "protocol=#{prot}"
puts "host=#{host}"
puts "username=#{user}"
puts "password=#{pass}"
exit(0)
end
end
将代码保存为 git-credential-read-only
,放到 PATH 路径下并给予执行权限。配置 Git 使用这个自定义助手:
powershellgit config credential.helper 'read-only --file /mnt/shared/creds'
给出答案
之所以会产生本文开头的问题,是因为 Git 默认使用 protocol 和 host 两个参数来获取用户凭证。只要是 https://github.com 下的 repo,都会使用相同的用户凭证。解决方法就是让 Git 在请求用户凭证的时候带上 username 参数,这样就能区分开不同的 GitHub 账号了。有两个方法可以用来指定 repo 的用户名:
方法1:在 repo 的 origin url 中指定用户名
powershellgit remote set-url origin https://<username>@github.com/<username>/<repo name>
方法2:在 repo 的 credential 中指定用户名
powershellgit config credential.username <username>