章节 ▾ 第二版

7.14 Git 工具 - 凭证存储

凭证存储

如果你使用 SSH 传输方式连接远程仓库,可以拥有一个没有密码短语的密钥,这允许你无需输入用户名和密码即可安全地传输数据。然而,对于 HTTP 协议来说,这不可能实现——每次连接都需要用户名和密码。对于启用双因素认证的系统来说,这变得更加困难,因为用作密码的令牌是随机生成且无法发音的。

幸运的是,Git 有一个凭证系统可以帮助解决这个问题。Git 内置了一些选项:

  • 默认情况下不进行任何缓存。每次连接都会提示你输入用户名和密码。

  • “cache”模式会将凭证在内存中保存一段时间。密码绝不会存储在磁盘上,并在15分钟后从缓存中清除。

  • “store”模式将凭证保存到磁盘上的一个纯文本文件中,并且永不过期。这意味着除非你更改 Git 主机的密码,否则你将无需再次输入凭证。这种方法的缺点是,你的密码以明文形式存储在主目录中的一个普通文件中。

  • 如果你使用的是 macOS,Git 提供了一个“osxkeychain”模式,它会将凭证缓存到附加到你的系统账户的安全钥匙串中。这种方法将凭证存储在磁盘上,并且永不过期,但它们使用与存储 HTTPS 证书和 Safari 自动填充相同的系统进行加密。

  • 如果你使用的是 Windows,在安装 Git for Windows 时可以启用 Git 凭证管理器 功能,或者单独安装 最新版 GCM 作为独立服务。这类似于上面描述的“osxkeychain”助手,但它使用 Windows 凭证存储来控制敏感信息。它还可以为 WSL1 或 WSL2 提供凭证。请参阅 GCM 安装说明 以获取更多信息。

你可以通过设置一个 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 会将用户名和密码发送给所有列表中的助手,它们可以选择如何处理这些信息。如果你的凭证文件在一个 U 盘上,但又想在 U 盘未插入时使用内存缓存来减少输入,那么 .gitconfig 文件会是这样:

[credential]
    helper = store --file /mnt/thumbdrive/.git-credentials
    helper = cache --timeout 30000

内部机制

这都如何工作呢?Git 凭证助手系统的根命令是 git credential,它接受一个命令作为参数,然后通过标准输入 (stdin) 获取更多输入。

通过一个示例可能更容易理解。假设凭证助手已配置,并且助手已为 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
  1. 这是启动交互的命令行。

  2. git-credential 随后在标准输入 (stdin) 上等待输入。我们向它提供我们已知的信息:协议和主机名。

  3. 一个空行表示输入完成,凭证系统应该用它所知道的信息来回答。

  4. git-credential 随后接管,并将其找到的信息写入标准输出 (stdout)。

  5. 如果未找到凭证,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-cachegit-credential-store 等等,我们可以配置它们接受命令行参数。其一般形式是“git-credential-foo [参数] <动作>。”标准输入/输出 (stdin/stdout) 协议与 git-credential 相同,但它们使用一套略有不同的动作:

  • get 是请求用户名/密码对。

  • store 是请求在此助手的内存中保存一组凭证。

  • erase 从此助手的内存中清除给定属性的凭证。

对于 storeerase 动作,不需要响应(Git 反正会忽略它)。然而,对于 get 动作,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
  1. 这里我们告诉 git-credential-store 保存一些凭证:当访问 https://mygithost 时,使用用户名“bob”和密码“s3cre7”。

  2. 现在我们来检索这些凭证。我们提供已知连接部分(https://mygithost),以及一个空行。

  3. git-credential-store 回复了我们上面存储的用户名和密码。

~/git.store 文件看起来像这样:

https://bob:s3cre7@mygithost

它只是一系列行,每行都包含一个带有凭证装饰的 URL。osxkeychainwincred 助手使用其后端存储的原生格式,而 cache 使用其自己的内存格式(其他进程无法读取)。

自定义凭证缓存

鉴于 git-credential-store 和类似的程序都独立于 Git,不难理解任何程序都可以成为 Git 凭证助手。Git 提供的助手涵盖了许多常见用例,但并非所有。例如,假设你的团队有一些与整个团队共享的凭证,可能用于部署。这些凭证存储在一个共享目录中,但你不想将它们复制到你自己的凭证存储中,因为它们经常更改。现有助手都无法满足这种情况;让我们看看编写自己的助手需要什么。这个程序需要具备几个关键特性:

  1. 我们唯一需要关注的动作是 getstoreerase 是写入操作,所以当它们被接收时,我们只需干净地退出。

  2. 共享凭证文件的格式与 git-credential-store 使用的格式相同。

  3. 该文件的位置相当标准,但我们应该允许用户传递自定义路径,以防万一。

再次强调,我们将用 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.exist? 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
  1. 这里我们解析命令行选项,允许用户指定输入文件。默认是 ~/.git-credentials

  2. 这个程序只在动作是 get 且后端存储文件存在时才响应。

  3. 这个循环从标准输入 (stdin) 读取,直到遇到第一个空行。输入被存储在 known 哈希中供以后参考。

  4. 这个循环读取存储文件的内容,查找匹配项。如果 known 中的协议、主机和用户名与此行匹配,程序将结果打印到标准输出 (stdout) 并退出。

我们将助手保存为 git-credential-read-only,将其放在我们的 PATH 中的某个位置并标记为可执行。这是一个交互式会话的示例:

$ git credential-read-only --file=/mnt/shared/creds get
protocol=https
host=mygithost
username=bob

protocol=https
host=mygithost
username=bob
password=s3cre7

因为它的名称以“git-”开头,我们可以使用配置值的简单语法:

$ git config --global credential.helper 'read-only --file /mnt/shared/creds'

如你所见,扩展这个系统非常直接,并且可以为你和你的团队解决一些常见问题。

scroll-to-top