Git
章节 ▾ 第二版

7.14 Git 工具 - 凭据存储

凭据存储

如果您使用 SSH 传输协议连接到远程仓库,您可以创建一个没有密码的密钥,这样您就可以在不输入用户名和密码的情况下安全地传输数据。但是,对于 HTTP 协议,这是不可能的,因为每个连接都需要用户名和密码。对于使用双因素身份验证的系统,情况就更复杂了,因为您用于密码的令牌是随机生成的,并且是不可读的。

幸运的是,Git 提供了一个凭据系统来帮助解决这个问题。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 会将用户名和密码发送到列表中的所有助手,并且助手可以选择如何处理它们。以下是一个 .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保存一些凭证:用户名“bob”和密码“s3cre7”将在访问https://mygithost时使用。

  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'

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

scroll-to-top