利用 Run Script 和 .gitignore 管理 API key,防止它上傳到 GitHub

最近彼得潘研究 Google AI SDK for Swift,發現了防止將私密的 API key 上傳到 GitHub 的不錯方法。

Google 管理 API key 的基本步驟跟原理如下。

  • 製作儲存 demo key 的 APIKey-Sample.plist。
  • 設定 Run Script,讓 Xcode 專案 build 時自動從 APIKey-Sample.plist 複製出新檔 APIKey.plist。
  • APIKey-Sample.plist 只是範本,真正的 API key 存在 APIKey.plist。
  • 在 .gitignore 加入 APIKey.plist,因此儲存真正 API key 的 APIKey.plist 不會上傳到 GitHub。

以下彼得潘將一步步示範如何用 Run Script 和 .gitignore 管理 API key。

新增 Group APIKey

製作儲存 demo key 的 APIKey-Sample.plist

用 TextEdit 新增文字檔,輸入以下內容,將 key 取名為 apiKey,value 設為 Demo。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>apiKey</key>
<string>Demo</string>
</dict>
</plist>

存檔時取名為 APIKey-Sample.plist,存在 ApiKey 資料夾下,取消 If no extension is provided, use .txt 的勾選。

專案 build 時自動從 APIKey-Sample.plist 複製新檔 APIKey.plist

透過設定 Run Script,我們可以讓 Xcode 在 build 專案時執行某些任務。以下我們將設定 Run Script,讓 Xcode 專案 build 時自動從 APIKey-Sample.plist 複製出新檔 APIKey.plist。APIKey-Sample.plist 只是範本,真正的 API key 將存在 APIKey.plist。

切換到 TARGETS 下 App 的 Build Phases 頁面,點選左上的 + 後點選 New Run Script Phase。

在 Run Script 的框框輸入以下 script。此段 script 的寫法參考 Google AI SDK,它會檢查 APIKey.plist 是否已存在,不存在時從 APIKey-Sample.plist 複製生成 APIKey.plist。

CONFIG_FILE_BASE_NAME="APIKey"

CONFIG_FILE_NAME=${CONFIG_FILE_BASE_NAME}.plist
SAMPLE_CONFIG_FILE_NAME=${CONFIG_FILE_BASE_NAME}-Sample.plist

CONFIG_FILE_PATH=$SRCROOT/APIKey/$CONFIG_FILE_NAME
SAMPLE_CONFIG_FILE_PATH=$SRCROOT/APIKey/$SAMPLE_CONFIG_FILE_NAME

if [ -f "$CONFIG_FILE_PATH" ]; then
echo "$CONFIG_FILE_PATH exists."
else
echo "$CONFIG_FILE_PATH does not exist, copying sample"
cp -v "${SAMPLE_CONFIG_FILE_PATH}" "${CONFIG_FILE_PATH}"
fi

在 Run Script 下方的 Output Files 加入 $(SRCROOT)/APIKey/APIKey.plist

將 Run Script 移動到 Compile Sources 前,確保 APIKey.plist 在編譯程式前生成。

將 User Script Sandboxing 設為 No

User Script Sandboxing 原本是 Yes,它會讓從 APIKey-Sample.plist 複製新檔 APIKey.plist 的動作失敗,build 時出現以下錯誤。

error: Sandbox: cp(2405) deny(1) file-read-data /Users/peter/Desktop/Demo/APIKey/APIKey-Sample.plist (in target 'Demo' from project 'Demo')
/Users/peter/Desktop/Demo/APIKey/APIKey.plist does not exist, copying sample
cp: /Users/peter/Desktop/Demo/APIKey/APIKey-Sample.plist: Operation not permitted

因此我們必須切換到 Build Settings 分頁,將 User Script Sandboxing 設為 No。

Apple 官方的 User Script Sandboxing 說明如下。

If enabled, the build system will sandbox user scripts to disallow undeclared input/output dependencies.

新增 .gitignore,避免 APIKey.plist 上傳到 GitHub

用 TextEdit 新增文字檔,輸入以下內容,讓 git 不要追蹤 APIKey.plist,

APIKey/APIKey.plist 

存檔時取名為 .gitignore,存在專案的資料夾下,取消 If no extension is provided, use txt 的勾選。

點選 Use .。

定義讀取 API key 的 enum APIKey

在 Group APIKey 下新增讀取 API key 的 APIKey.swift

定義 enum APIKeystatic property default,之後 App 透過 APIKey.default 即可讀取 APIKey.plist 儲存的真正 API key。

  • 寫法 1。

將 default 宣告為 computed property。

import Foundation
import UniformTypeIdentifiers

enum APIKey {
struct ApiKeyData: Decodable {
let apiKey: String
}

static var `default`: String {
guard let fileURL = Bundle.main.url(forResource: "\(Self.self)", withExtension: UTType.propertyList.preferredFilenameExtension) else {
fatalError("Couldn't find file APIKey.plist")
}
guard let data = try? Data(contentsOf: fileURL) else {
fatalError("Couldn't read data from APIKey.plist")
}
guard let apiKey = try? PropertyListDecoder().decode(ApiKeyData.self, from: data).apiKey else {
fatalError("Couldn't find key apiKey")
}
return apiKey
}
}
  • 寫法 2。

將 default 宣告為 stored property。

import Foundation
import UniformTypeIdentifiers

enum APIKey {
struct ApiKeyData: Decodable {
let apiKey: String
}

static var `default` = {
guard let fileURL = Bundle.main.url(forResource: "\(Self.self)", withExtension: UTType.propertyList.preferredFilenameExtension) else {
fatalError("Couldn't find file APIKey.plist")
}
guard let data = try? Data(contentsOf: fileURL) else {
fatalError("Couldn't read data from APIKey.plist")
}
guard let apiKey = try? PropertyListDecoder().decode(ApiKeyData.self, from: data).apiKey else {
fatalError("Couldn't find key apiKey")
}
return apiKey
}()
}

build 專案生成 APIKey.plist

將 APIKey.plist 加到 Xcode 專案的 APIKey 下

從 APIKey 的右鍵選單點選 Add Files to Demo

選擇 APIKey.plist,點選 Add。

APIKey 下成功出現 APIKey.plist。

輸入真正的 API key

現在我們可以放心地在 APIKey.plist 設定真正的 API key,比方我愛 Apple。我們有設定 .gitignore,因此不用擔心 APIKey.plist 上傳到 GitHub。

如下圖所示,APIKey.plist 旁邊沒有出現 A,表示它沒有被 git 追蹤。

將專案上傳到 GitHub

從 Xcode commit 的畫面可看到 APIKey.plist 果然沒有被 git 追蹤。

GitHub 上只有 APIKey-Sample.plist,沒有 APIKey.plist,終於不用擔心 API key 外洩了。

重新 clone 專案

當我們重新 clone 專案時,一開始 APIKey.plist 是紅色的,因為它尚未生成。

別擔心,第一次 Build 後即可生成 APIKey.plist,在裡面設定不可告人的 API key。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com