-
1. 起步
-
2. Git 基础
-
3. Git 分支
-
4. 服务器上的 Git
- 4.1 协议
- 4.2 在服务器上部署 Git
- 4.3 生成 SSH 公钥
- 4.4 架设服务器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管服务
- 4.10 小结
-
5. 分布式 Git
-
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 协议无法做到这一点——每次连接都需要用户名和密码。对于使用双重认证(2FA)的系统来说,这就变得更加困难,因为你用作密码的令牌通常是随机生成且难以读出的。
幸运的是,Git 提供了一套凭证系统来解决这个问题。Git 内置了几个选项:
-
默认设置是不进行任何缓存。每次连接时,系统都会提示你输入用户名和密码。
-
“cache” 模式会将凭证存储在内存中一段时间。密码绝不会存储在磁盘上,并且会在 15 分钟后从缓存中清除。
-
“store” 模式会将凭证以明文形式保存到磁盘文件,且永不过期。这意味着在你更改 Git 主机的密码之前,你无需再次输入凭证。这种方法的缺点是密码以明文形式存储在主目录的一个普通文件中。
-
如果你使用的是 macOS,Git 自带了“osxkeychain”模式,它会将凭证缓存到与你的系统账户关联的安全钥匙串(Keychain)中。这种方法会将凭证存储在磁盘上,且永不过期,但它们会通过存储 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 会将用户名和密码发送给列表中的所有助手,由它们决定如何处理。如果你有一个存储在 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 的配置值。它可以有几种形式:
| 配置值 | 行为 |
|---|---|
|
运行 |
|
运行 |
|
运行 |
|
|
因此,上述提到的助手实际上名为 git-credential-cache、git-credential-store 等,我们可以将它们配置为接受命令行参数。其通用形式为 “git-credential-foo [args] <action>”。标准输入/输出协议与 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'
如你所见,扩展这个系统非常直观,可以为你和你的团队解决一些常见问题。