章節 ▾ 第二版

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'

如您所見,擴充套件此係統非常簡單,並且可以為您和您的團隊解決一些常見問題。