-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: Git 命令
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
-
这是启动交互的命令行。
-
git-credential
随后在标准输入 (stdin) 上等待输入。我们向它提供我们已知的信息:协议和主机名。 -
一个空行表示输入完成,凭证系统应该用它所知道的信息来回答。
-
git-credential
随后接管,并将其找到的信息写入标准输出 (stdout)。 -
如果未找到凭证,Git 会要求用户输入用户名和密码,并将它们提供回调用方的标准输出(这里它们连接到同一个控制台)。
凭证系统实际上是调用一个独立于 Git 本身的程序;调用哪个程序以及如何调用取决于 credential.helper
配置值。它有几种形式:
配置值 | 行为 |
---|---|
|
运行 |
|
运行 |
|
运行 |
|
! 后的代码在 shell 中评估 |
所以上面描述的助手实际上被命名为 git-credential-cache
、git-credential-store
等等,我们可以配置它们接受命令行参数。其一般形式是“git-credential-foo [参数] <动作>。”标准输入/输出 (stdin/stdout) 协议与 git-credential
相同,但它们使用一套略有不同的动作:
-
get
是请求用户名/密码对。 -
store
是请求在此助手的内存中保存一组凭证。 -
erase
从此助手的内存中清除给定属性的凭证。
对于 store
和 erase
动作,不需要响应(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
-
这里我们告诉
git-credential-store
保存一些凭证:当访问 https://mygithost 时,使用用户名“bob”和密码“s3cre7”。 -
现在我们来检索这些凭证。我们提供已知连接部分(https://mygithost),以及一个空行。
-
git-credential-store
回复了我们上面存储的用户名和密码。
~/git.store
文件看起来像这样:
https://bob:s3cre7@mygithost
它只是一系列行,每行都包含一个带有凭证装饰的 URL。osxkeychain
和 wincred
助手使用其后端存储的原生格式,而 cache
使用其自己的内存格式(其他进程无法读取)。
自定义凭证缓存
鉴于 git-credential-store
和类似的程序都独立于 Git,不难理解任何程序都可以成为 Git 凭证助手。Git 提供的助手涵盖了许多常见用例,但并非所有。例如,假设你的团队有一些与整个团队共享的凭证,可能用于部署。这些凭证存储在一个共享目录中,但你不想将它们复制到你自己的凭证存储中,因为它们经常更改。现有助手都无法满足这种情况;让我们看看编写自己的助手需要什么。这个程序需要具备几个关键特性:
-
我们唯一需要关注的动作是
get
;store
和erase
是写入操作,所以当它们被接收时,我们只需干净地退出。 -
共享凭证文件的格式与
git-credential-store
使用的格式相同。 -
该文件的位置相当标准,但我们应该允许用户传递自定义路径,以防万一。
再次强调,我们将用 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
-
这里我们解析命令行选项,允许用户指定输入文件。默认是
~/.git-credentials
。 -
这个程序只在动作是
get
且后端存储文件存在时才响应。 -
这个循环从标准输入 (stdin) 读取,直到遇到第一个空行。输入被存储在
known
哈希中供以后参考。 -
这个循环读取存储文件的内容,查找匹配项。如果
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'
如你所见,扩展这个系统非常直接,并且可以为你和你的团队解决一些常见问题。