章節 ▾ 第二版

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
  1. 這是啟動互動的命令列。

  2. git-credential 隨後在標準輸入 (stdin) 上等待輸入。我們向它提供我們已知的資訊:協議和主機名。

  3. 一個空行表示輸入完成,憑證系統應該用它所知道的資訊來回答。

  4. git-credential 隨後接管,並將其找到的資訊寫入標準輸出 (stdout)。

  5. 如果未找到憑證,Git 會要求使用者輸入使用者名稱和密碼,並將它們提供回撥用方的標準輸出(這裡它們連線到同一個控制檯)。

憑證系統實際上是呼叫一個獨立於 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 [引數] <動作>。”標準輸入/輸出 (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. 我們唯一需要關注的動作是 getstoreerase 是寫入操作,所以當它們被接收時,我們只需乾淨地退出。

  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