章节 ▾ 第二版

7.14 Git 工具 - 凭据存储

凭据存储

如果你使用 SSH 传输协议连接到远程仓库,你可以使用没有密码的密钥,这样就可以安全地传输数据,而无需输入用户名和密码。但是,HTTP 协议是不可能的,每次连接都需要用户名和密码。对于具有双重身份验证的系统来说,这会变得更加困难,因为你用于密码的令牌是随机生成的并且难以发音。

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

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

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

  • “store” 模式将凭据保存到磁盘上的纯文本文件中,并且它们永不过期。这意味着,在你更改 Git 主机的密码之前,你永远不必再次输入凭据。这种方法的缺点是你的密码以明文形式存储在你主目录下的一个纯文本文件中。

  • 如果你使用的是 macOS,Git 附带“osxkeychain”模式,该模式将凭据缓存在附加到你的系统帐户的安全密钥链中。此方法将凭据存储在磁盘上,并且它们永不过期,但是它们使用存储 HTTPS 证书和 Safari 自动填充的相同系统进行加密。

  • 如果你使用的是 Windows,则可以在安装 Git for Windows 时启用 Git Credential Manager 功能,也可以单独安装最新的 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 会将用户名和密码发送到列表中**所有**的助手,它们可以选择如何处理这些凭据。如果你的拇指驱动器上有一个凭据文件,但又想使用内存中的缓存来节省一些打字时间(如果驱动器没有插入),那么 .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 会要求用户提供用户名和密码,并将它们返回到调用的 stdout(在这里它们附加到同一个控制台)。

凭据系统实际上是在调用一个独立于 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 [args] <action>”。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