# 7.14 Git 工具 - 凭证存储
# 凭证存储
如果你使用的是 SSH 方式连接远端,并且设置了一个没有口令的密钥,这样就可以在不输入用户名和密码的情况下安全地传输数据。 然而,这对 HTTP 协议来说是不可能的 —— 每一个连接都是需要用户名和密码的。 这在使用双重认证的情况下会更麻烦,因为你需要输入一个随机生成并且毫无规律的 token 作为密码。
幸运的是,Git 拥有一个凭证系统来处理这个事情。 下面有一些 Git 的选项:
默认所有都不缓存。 每一次连接都会询问你的用户名和密码。
“cache” 模式会将凭证存放在内存中一段时间。 密码永远不会被存储在磁盘中,并且在15分钟后从内存中清除。
“store” 模式会将凭证用明文的形式存放在磁盘中,并且永不过期。 这意味着除非你修改了你在 Git 服务器上的密码,否则你永远不需要再次输入你的凭证信息。 这种方式的缺点是你的密码是用明文的方式存放在你的 home 目录下。
如果你使用的是 Mac,Git 还有一种 “osxkeychain” 模式,它会将凭证缓存到你系统用户的钥匙串中。 这种方式将凭证存放在磁盘中,并且永不过期,但是是被加密的,这种加密方式与存放 HTTPS 凭证以及 Safari 的自动填写是相同的。
如果你使用的是 Windows,你可以安装一个叫做 “Git Credential Manager for Windows” 的辅助工具。 这和上面说的 “osxkeychain” 十分类似,但是是使用 Windows Credential Store 来控制敏感信息。 可以在
https://github.com/Microsoft/Git-Credential-Manager-for-Windows
下载。
你可以设置 Git 的配置来选择上述的一种方式
$ git config --global credential.helper cache
部分辅助工具有一些选项。 “store” 模式可以接受一个 --file <path>
参数,可以自定义存放密码的文件路径(默认是 ~/.git-credentials
)。 “cache” 模式有 --timeout <seconds>
参数,可以设置后台进程的存活时间(默认是 “900”,也就是 15 分钟)。 下面是一个配置 “store” 模式自定义路径的例子:
$ git config --global credential.helper 'store --file ~/.my-credentials'
Git 甚至允许你配置多个辅助工具。 当查找特定服务器的凭证时,Git 会按顺序查询,并且在找到第一个回答时停止查询。
当保存凭证时,Git 会将用户名和密码发送给 所有配置列表中的辅助工具,它们会按自己的方式处理用户名和密码。 如果你在闪存上有一个凭证文件,但又希望在该闪存被拔出的情况下使用内存缓存来保存用户名密码,.gitconfig
配置文件如下:
[credential] helper = store --file /mnt/thumbdrive/.git-credentials helper = cache --timeout 30000
# 底层实现
这些是如何实现的呢? Git 凭证辅助工具系统的命令是 git credential
,这个命令接收一个参数,并通过标准输入获取更多的参数。
举一个例子更容易理解。 我们假设已经配置好一个凭证辅助工具,这个辅助工具保存了 mygithost
的凭证信息。 下面是一个使用 “fill” 命令的会话,当 Git 尝试寻找一个服务器的凭证时就会被调用。
$ git credential fill (1)
protocol=https (2)
host=mygithost
(3)
protocol=https (4)
host=mygithost
username=bob
password=s3cre7
$ git credential fill (5)
protocol=https
host=unknownhost
Username for 'https://unknownhost': bob
Password for 'https://bob@unknownhost':
protocol=https
host=unknownhost
username=bob
password=s3cre7
这是开始交互的命令。
Git-credential 接下来会等待标准输入。 我们提供我们所知道的信息:协议和主机名。
一个空行代表输入已经完成,凭证系统应该输出它所知道的信息。
接下来由 Git-credential 接管,并且将找到的信息打印到标准输出。
如果没有找到对应的凭证,Git 会询问用户的用户名和密码,我们将这些信息输入到在标准输出的地方(这个例子中是同一个控制台)。
凭证系统实际调用的程序和 Git 本身是分开的;具体是哪一个以及如何调用与 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 执行 |
上面描述的辅助工具可以被称做 git-credential-cache
、git-credential-store
之类,我们可以配置它们来接受命令行参数。 通常的格式是 “git-credential-foo [args] <action>
”
标准输入/输出协议和 git-credential 一样,但它们使用的是一套稍微不太一样的行为:
get
是请求输入一对用户名和密码。store
是请求保存一个凭证到辅助工具的内存。erase
会将给定的证书从辅助工具内存中清除。
对于 store
和 erase
两个行为是不需要返回数据的(Git 也会忽略掉)。 然而对于 get
,Git 对辅助工具的返回信息十分感兴趣。 如果辅助工具并不知道任何有用的信息,它就会直接退出而没有任何输出,但如果知道的话,
它就会在已存储信息的基础上扩充所提供的信息。
它的输出可看做一系列赋值语句,提供的任何内容都会取代 Git 已知的内容。
如果辅助工具没有任何有用的信息,它可以直接退出而不需要输出任何东西,但如果它有这些信息,它在提供的信息后面增加它所拥有的信息。 这些输出会被视为一系列的赋值语句;每一个提供的数据都会将 Git 已有的数据替换掉。
这有一个和上面一样的例子,但是跳过了 git-credential 这一步,直接到 git-credential-store:
$ git credential-store --file ~/git.store store (1)
protocol=https
host=mygithost
username=bob
password=s3cre7
$ git credential-store --file ~/git.store get (2)
protocol=https
host=mygithost
username=bob (3)
password=s3cre7
我们告诉
git-credential-store
去保存凭证:当访问https://mygithost
时使用用户名 “bob”,密码是 “s3cre7”。现在我们取出这个凭证。 我们提供连接这部分的信息(
https://mygithost
)以及一个空行。git-credential-store
输出我们之前保存的用户名和密码。
~/git.store
文件的内容类似:
https://bob:s3cre7@mygithost
仅仅是一系列包含凭证信息 URL 组成的行。 osxkeychain
和 wincred
辅助工具使用它们后端存储的原生格式,而 cache
使用它的内存格式(其他进程无法读取)。
# 自定义凭证缓存
已经知道 git-credential-store
之类的是和 Git 是相互独立的程序,就不难理解 Git 凭证辅助工具可以是 任意程序。 虽然 Git 提供的辅助工具覆盖了大多数常见的使用场景,但并不能满足所有情况。
比如,假设你的整个团队共享一些凭证,也许是在部署时使用。
这些凭证是保存在一个共享目录里,由于这些凭证经常变更,所以你不想把它们复制到你自己的凭证仓库中。
现有的辅助工具无法满足这种情况;来看看我们如何自己实现一个。
这个程序应该拥有几个核心功能:
我们唯一需要关注的行为是
get
;store
和erase
是写操作,所以当接受到这两个请求时我们直接退出即可。共享的凭证文件格式和
git-credential-store
使用的格式相同。凭证文件的路径一般是固定的,但我们应该允许用户传入一个自定义路径以防万一。
我们再一次使用 Ruby 来编写这个扩展,但只要 Git 能够执行最终的程序,任何语言都是可以的。 这是我们的凭证辅助工具的完整代码:
#!/usr/bin/env ruby
require 'optparse'
path = File.expand_path '~/.git-credentials' # (1)
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' # (2)
exit(0) unless File.exists? path
known = {} # (3)
while line = STDIN.gets
break if line.strip == ''
k,v = line.strip.split '=', 2
known[k] = v
end
File.readlines(path).each do |fileline| # (4)
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-credentials
.这个程序只有在接受到
get
行为的请求并且后端存储的文件存在时才会有输出。这个循环从标准输入读取数据,直到读取到第一个空行。 输入的数据被保存到
known
哈希表中,之后需要用到。这个循环读取存储文件中的内容,寻找匹配的行。 如果
known
中的协议和主机名与该行相匹配,这个程序输出结果并退出。
我们把这个辅助工具保存为 git-credential-read-only
,放到我们的 PATH
路径下并且给予执行权限。 一个交互式会话类似:
$ git credential-read-only --file=/mnt/shared/creds get
protocol=https
host=mygithost
protocol=https
host=mygithost
username=bob
password=s3cre7
由于这个的名字是 “git-” 开头,所以我们可以在配置值中使用简便的语法:
$ git config --global credential.helper 'read-only --file /mnt/shared/creds'
正如你看到的,扩展这个系统是相当简单的,并且可以为你和你的团队解决一些常见问题。