章节 ▾ 第二版

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 Credential Store 来控制敏感信息。它还可以为 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,它接受一个命令作为参数,然后通过标准输入接收更多输入。

通过示例可能更容易理解。假设凭据助手已配置,并且助手已存储 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 随后等待标准输入。我们向它提供我们已知的信息:协议和主机名。

  3. 一个空行表示输入完成,凭据系统应回答它所知道的。

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

  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 [args] <action>”。标准输入/标准输出协议与 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. 我们只需要关注 get 动作;storeerase 是写入操作,所以当它们被接收时,我们只需干净地退出。

  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. 此循环从标准输入读取,直到遇到第一个空行为止。输入存储在 known 哈希中以供后续引用。

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

我们将助手保存为 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'

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