有空在 GitHub 搜尋 "API keys" plaintext is:issue。你會看到許多真實部署、多使用者應用程式的維護者寫下像「金鑰目前以明文儲存在資料庫中,這對平台是個風險」這樣的句子。我過去一個月讀過幾十篇這樣的 issue,因為我一直在寄信給寫這些話的人。
需求面很容易解釋。使用者要求 BYOK(Bring Your Own Key,自帶金鑰),是因為他們已經在別處付費使用 AI、希望提示在自己的供應商帳戶上執行,或是因為免費額度的速率限制讓他們感到困擾。開發者想要 BYOK,是因為推論成本會隨使用量而增加,而收入卻不一定跟上。
因此 BYOK 不斷被提出,也不斷被實作得很差。以下是我在實際案例中看到過的四個等級,從最差到生產等級排序。
等級 0:資料庫中的明文欄位
這種做法比任何人願意承認的還要常見。有一個 users.openai_api_key 欄位,儲存時寫入、每次請求時讀取。
這有個可以理解的原因:這個功能只要一個下午就能完成。但供應商金鑰不像密碼雜湊,它是一組可立即使用的、能花錢的憑證。如果你的資料庫透過備份、設定錯誤的管理面板,或是一次注入漏洞外洩,攻擊者拿到的不是需要破解的雜湊,而是附帶帳單的可用金鑰,涵蓋你所有的使用者。
如果你今天停留在等級 0,下面提到的加密欄位升級只需要一天就能完成。建議這週就做。
等級 1:環境變數
部署環境中的 OPENAI_API_KEY。對單一租戶來說沒問題,適合自架者與內部工具,由運營者持有金鑰。但一旦你提供託管且有多使用者,單一環境變數就代表所有使用者共用同一個帳戶、同一個速率限制、同一個帳單。這正是 BYOK 想要解決的問題。
等級 2:金鑰留在瀏覽器
使用 localStorage 或 IndexedDB,在每次請求時由客戶端附加金鑰,伺服器端不儲存任何東西。本地優先的工具會這樣做,對它們來說是正確的選擇。你的伺服器永遠不會洩漏它從未看過的東西。
問題出在產品成長後需要伺服器端功能。背景工作、排程執行、webhook、團隊工作區、手機客戶端。如果金鑰只存在於單一瀏覽器分頁,這些功能就無法運作。等級 2 對本地工具是可行的答案,但對託管 SaaS 來說是死胡同。
等級 3:伺服器端加密保險庫
這是託管、多租戶產品的答案,也是真正需要花最多心力的地方。「加密金鑰」聽起來只有一行加密程式碼,但實際上加密部分大概只有二十行,周邊的一切才是產品。
以下是我認為生產等級 BYOK 實作應該滿足的最低檢查清單:
加密
- AES-256-GCM,每次加密使用獨一無二的 nonce,同一個金鑰絕不重複使用 nonce。
- 加密金鑰存放於資料庫之外。使用 KMS、secrets manager,或至少放在與資料不同信任邊界的環境變數中。
- 將密文綁定到其對應的資料列(使用 AAD 加上使用者或連線 ID),讓擁有資料庫寫入權限的攻擊者無法在不同資料列之間交換密文。
使用者體驗規則
- 寫入專用。儲存後,客戶端永遠拿不回金鑰。只顯示
sk-...abc4之類的部分內容。 - 更換金鑰需要重新輸入,而非編輯。
運維衛生
- 在請求時、只在呼叫供應商的程式碼路徑中進行解密。明文金鑰絕不應出現在日誌、錯誤報告或分析事件中。供應商上游錯誤有時會回傳請求標頭,也應一併遮蔽。
生命週期(大家最容易低估的部分)
- 撤銷必須立即生效。當使用者點擊中斷連線時,只有正在進行中的流量可以繼續。
- 使用量歸屬。當有人問「哪個使用者昨天花了 40 美元」,你需要有答案,這表示需要記錄每次請求、每個使用者的 token 數量。
- 輪替。使用者會更換供應商金鑰,你的產品需要提供輪替途徑,而不需透過客服工單。
一個能找出大部分錯誤實作的快速自我測試:你的客服人員是否能看到金鑰?金鑰是否出現在日誌中?如果今晚資料庫外洩,攻擊者到底拿到了什麼?使用者是否能一鍵終止存取權限?你是否能知道哪個使用者花了多少錢?如果其中任何答案讓你感到不舒服,就表示你還沒完成。
購買選項
先聲明我的立場:我自己做過這樣的產品,所以本節的其餘部分算是推廣。
Monet 是一個以託管服務形式提供的等級 3 保險庫,設計成類似 OAuth 的形式,因為這是使用者已經信任的授權流程。使用者進入託管的連線頁面,連線他們的 ChatGPT Plus 或 Claude Pro 帳戶,或貼上自己的供應商金鑰。憑證會以 AES-256-GCM 加密存入保險庫,你的應用程式永遠不會接觸到它。你的應用程式透過標準的授權碼交換取得不透明的 bearer token,並呼叫一個相容 OpenAI 的端點:
from openai import OpenAI
client = OpenAI(
base_url="https://monet.gg/api/v1",
api_key=monet_access_token, # opaque token from the OAuth exchange, not a provider key
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hello"}],
)
Enter fullscreen mode Exit fullscreen mode
代理伺服器會將 token 解析成對應使用者的憑證,將回應串流傳回,並寫入由供應商回報的真實 token 數量的使用事件,因此計量與成本轉嫁都免費提供。撤銷連線時會清除保險庫資料列,token 也會停止解析。伺服器端只儲存 token 的 SHA-256 雜湊,即使 Monet 自己的 token 表格被完整傾印,也無法被用來花費。
目前在 beta 期間免費使用。在 demo.monet.gg 有一個示範應用程式展示完整流程,如果你想從頭看到尾看看,而開發者儀表板則在 monet.gg。
如果你寧願自己實作,上面列出的檢查清單就是誠實的最低需求,我也很樂意在任何一種方式下交流。我這個月已經讀過太多明文金鑰的 issue,希望能減少這類問題的發生。
相關文章:I'm 15 and I built "OAuth for AI subscriptions". Here's the full architecture 是託管選項的完整設計,而 Stop paying for your users' AI usage 則是 BYOK 的經濟論點。
我是 Shlok,15 歲,獨力開發 Monet。我會閱讀每一則回覆:[email protected],github.com/shlok-madhekar,@shlokbuilds。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.