章节 ▾ 第二版

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,它接受一个命令作为参数,然后通过 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. 我们只需要关注 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. 此循环从 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'

如您所见,扩展此系统非常简单,并且可以为您和您的团队解决一些常见问题。