别再往 .env 文件里直接塞 API 密钥了!系统自带密钥链才是安全王道
问你一个扎心的问题:你电脑上现在有多少个 .env 文件,里面明文躺着一个活生生的 OPENAI_API_KEY?说实话。
我比你想象的多得多。每个项目一个,~/Downloads 里还有几个测试时留下的,.bash_history 里甚至还有因为赶时间直接敲 export OPENAI_API_KEY=sk-... 留下的印记。
每一个都是未加密的密钥躺在磁盘上,等着被某个错误的脚本 grep 到、同步到错误的备份、或者粘贴到错误的屏幕共享里。
更好的方法一直就在你的操作系统里。每台主流系统都自带一个安全凭据存储:macOS 有 Keychain,Linux 有 Secret Service API,Windows 有 Credential Manager。本文教你如何把你的 AI API 密钥(Gemini、OpenAI、Anthropic、xAI、DeepSeek……随便什么)放进去,并让它们在 Shell 启动时自动加载,这样:
- 密钥永远不会出现在明文文件中。
- 密钥永远不会出现在你的 Shell 历史里。
- 轮换密钥只需修改一处。
- 负责加载密钥的配置文件里只包含密钥的名称,绝不包含值,因此可以放心提交到 dotfiles 仓库。
一句话总结
用系统自带的 CLI 把密钥存入操作系统密钥链,然后放一个很小的加载脚本,在 Shell 启动时把它们读成环境变量。macOS 用 security,Linux 用 secret-tool,Windows 用 PowerShell 操作 Credential Manager。直接跳到你的平台章节。
下面是我用作示例的密钥:
GEMINI_API_KEY
OPENAI_API_KEY
ANTHROPIC_API_KEY
XAI_API_KEY
DEEPSEEK_API_KEY
把你自己实际用的替换掉即可。
macOS
macOS 提供了 security 命令行工具,它操作的就是你平时用来存 Wi-Fi 密码和 Safari 登录信息的那个钥匙串。
存储密钥
每个密钥运行一次。-w 参数后面不加值,会让它交互式地提示你输入密钥,这样密钥就不会落入 Shell 历史。
security add-generic-password -a "$USER" -s "GEMINI_API_KEY" -w
security add-generic-password -a "$USER" -s "OPENAI_API_KEY" -w
security add-generic-password -a "$USER" -s "ANTHROPIC_API_KEY" -w
security add-generic-password -a "$USER" -s "XAI_API_KEY" -w
security add-generic-password -a "$USER" -s "DEEPSEEK_API_KEY" -w
-a "$USER"是账户名(你的登录名)-s "..."是服务名(以后查找时用的标签)-w表示交互式输入密钥
如果密钥已存在,你会得到一个错误。先删除(见下),再重新添加。
读取、更新、删除
# 读取
security find-generic-password -a "$USER" -s "GEMINI_API_KEY" -w
# 更新(先删后加)
security delete-generic-password -a "$USER" -s "GEMINI_API_KEY"
security add-generic-password -a "$USER" -s "GEMINI_API_KEY" -w
# 删除
security delete-generic-password -a "$USER" -s "GEMINI_API_KEY"
加载到 Shell
创建 ~/.api-keys.sh。注意它只包含密钥名称,没有秘密值:
cat > ~/.api-keys.sh <<'EOF'
#!/usr/bin/env bash
# 从 macOS Keychain 加载 API 密钥到环境变量
keychain_load() {
local var="$1" val
val=$(security find-generic-password -a "$USER" -s "$var" -w 2>/dev/null)
[[ -n "$val" ]] && export "$var=$val"
}
keychain_load GEMINI_API_KEY
keychain_load OPENAI_API_KEY
keychain_load ANTHROPIC_API_KEY
keychain_load XAI_API_KEY
keychain_load DEEPSEEK_API_KEY
EOF
在 ~/.bash_profile(如果你用的是 zsh,macOS 默认,就用 ~/.zshrc)里 source 它:
echo '[[ -f ~/.api-keys.sh ]] && source ~/.api-keys.sh' >> ~/.zshrc
首次运行注意事项
第一次 Shell 读取密钥时,macOS 会弹出一个对话框,询问是否允许访问钥匙串中的这项。点击始终允许,以后就不会再问了。这算是一个有意的权衡:方便换来的是任何以你身份运行的进程都能读取这些密钥。对于开发者笔记本来说,这是一个合理的边界。
Linux
Linux 使用 Secret Service API,通过 libsecret 接入 GNOME Keyring、KWallet 或 KeePassXC。大多数桌面发行版都已经有一个钥匙串守护进程在运行。
安装工具
# Debian / Ubuntu
sudo apt install libsecret-tools
# Fedora / RHEL
sudo dnf install libsecret
# Arch
sudo pacman -S libsecret
存储密钥
secret-tool store 会从输入提示读取密钥,避免留在历史中。--label 只是给人看的描述,service 和 key 属性是以后查找用的。
secret-tool store --label="GEMINI_API_KEY" service api_keys key GEMINI_API_KEY
secret-tool store --label="OPENAI_API_KEY" service api_keys key OPENAI_API_KEY
secret-tool store --label="ANTHROPIC_API_KEY" service api_keys key ANTHROPIC_API_KEY
secret-tool store --label="XAI_API_KEY" service api_keys key XAI_API_KEY
secret-tool store --label="DEEPSEEK_API_KEY" service api_keys key DEEPSEEK_API_KEY
读取、更新、删除
# 读取
secret-tool lookup service api_keys key GEMINI_API_KEY
# 更新(store 会覆盖已有条目,直接再存一次即可)
secret-tool store --label="GEMINI_API_KEY" service api_keys key GEMINI_API_KEY
# 删除
secret-tool clear service api_keys key GEMINI_API_KEY
加载到 Shell
创建 ~/.api-keys.sh:
cat > ~/.api-keys.sh <<'EOF'
#!/usr/bin/env bash
# 从 Secret Service (libsecret) 加载 API 密钥到环境变量
keychain_load() {
local var="$1" val
val=$(secret-tool lookup service api_keys key "$var" 2>/dev/null)
[[ -n "$val" ]] && export "$var=$val"
}
keychain_load GEMINI_API_KEY
keychain_load OPENAI_API_KEY
keychain_load ANTHROPIC_API_KEY
keychain_load XAI_API_KEY
keychain_load DEEPSEEK_API_KEY
EOF
在 ~/.bashrc(或 ~/.zshrc)里 source 它:
echo '[[ -f ~/.api-keys.sh ]] && source ~/.api-keys.sh' >> ~/.bashrc
无图形界面的服务器
在服务器上没有图形会话时,通常也没有钥匙串守护进程,因此这个方法不适用。对于服务器,请使用 pass(基于 GPG 的 Unix 密码管理器)或正规的密钥管理工具。最后有更多说明。
Windows
Windows 自带 Credential Manager。PowerShell 是最干净的驱动方式。
存储密钥
将下面的脚本保存为 Set-ApiKey.ps1。它用 Read-Host -AsSecureString 提示输入值,因此密钥永远不会显示在终端或历史中:
param([Parameter(Mandatory)][string]$Name)
$secret = Read-Host -AsSecureString "Enter value for $Name"
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret))
cmdkey /generic:$Name /user:apikey /pass:$plain | Out-Null
Remove-Variable plain, secret
Write-Host "Stored $Name in Credential Manager"
然后对每个密钥运行:
.\Set-ApiKey.ps1 GEMINI_API_KEY
.\Set-ApiKey.ps1 OPENAI_API_KEY
.\Set-ApiKey.ps1 ANTHROPIC_API_KEY
.\Set-ApiKey.ps1 XAI_API_KEY
.\Set-ApiKey.ps1 DEEPSEEK_API_KEY
读取和删除
cmdkey 本身不会把秘密值返回,所以要用 CredentialManager 模块来编程读取:
Install-Module CredentialManager -Scope CurrentUser # 只需安装一次
$cred = Get-StoredCredential -Target GEMINI_API_KEY
$cred.GetNetworkCredential().Password
# 删除
cmdkey /delete:GEMINI_API_KEY
加载到 Shell
将以下内容添加到你的 PowerShell 配置文件(运行 notepad $PROFILE 打开,如果不存在就创建):
# 需要先执行: Install-Module CredentialManager -Scope CurrentUser
$apiKeys = @(
'GEMINI_API_KEY','OPENAI_API_KEY','ANTHROPIC_API_KEY',
'XAI_API_KEY','DEEPSEEK_API_KEY'
)
foreach ($k in $apiKeys) {
$cred = Get-StoredCredential -Target $k -ErrorAction SilentlyContinue
if ($cred) {
[Environment]::SetEnvironmentVariable(
$k, $cred.GetNetworkCredential().Password, 'Process')
}
}
WSL 注意事项
如果你在 WSL 中工作,请把它当成 Linux:安装 libsecret-tools,然后按照 Linux 章节操作。WSL 不会自动共享 Windows 的 Credential Manager。
使用密钥
加载后,它们就是普通的环境变量。任何能读取环境变量的程序都可以使用:
echo "${GEMINI_API_KEY:0:6}..." # 只显示前6个字符,用于验证
python script.py # os.environ["OPENAI_API_KEY"]
node app.js # process.env.OPENAI_API_KEY
给 Laravel 开发者的提示
Laravel 会读取 .env,但 vlucas/phpdotenv 不会覆盖已经存在的环境变量。所以从终端运行的 php artisan 命令会继承你的 Shell 环境,只要满足以下条件,env('GEMINI_API_KEY') 就能正常工作,而密钥不必出现在 .env 中:
- 把密钥从
.env中移除。 - 在
config/services.php中用env('GEMINI_API_KEY')引用它。 - 本地开发时避免运行
php artisan config:cache(它会冻结值;轮换密钥后运行php artisan config:clear)。
但要注意:不继承 Shell 的进程(比如 Herd PHP-FPM、supervisor 管理的队列工人、Docker)看不到这些密钥。对于它们,要么在对应工具的配置中设置环境变量,要么写一个小脚本从钥匙串生成 .env 条目。不论哪种方式,都要把 .env 保留在 .gitignore 中。
验证是否全部加载
macOS 和 Linux:
for k in GEMINI OPENAI ANTHROPIC XAI DEEPSEEK; do
var="${k}_API_KEY"
if [[ -n "${!var}" ]]; then
echo "$var: ${!var:0:6}... OK"
else
echo "$var: MISSING"
fi
done
Windows PowerShell:
'GEMINI_API_KEY','OPENAI_API_KEY','ANTHROPIC_API_KEY','XAI_API_KEY','DEEPSEEK_API_KEY' | ForEach-Object {
$val = [Environment]::GetEnvironmentVariable($_, 'Process')
if ($val) { "{0}: {1}... OK" -f $_, $val.Substring(0,6) }
else { "$_`: MISSING" }
}
这种方法的边界
请明确它能保护什么、不能保护什么。
钥匙串保护的是静态的密钥。一旦密钥被加载到环境变量中,任何以你用户身份运行的进程都可以读取它。这对开发者笔记本来说没问题,但绝对不适合生产服务器或 CI 流水线——那里应该使用真正的密钥管理工具:HashiCorp Vault、AWS Secrets Manager、Doppler 或你的 CI 平台自带的密钥管理。这个方案只适用于你的笔记本,仅此而已。
几个保持干净的好习惯:
- 轮换密钥时直接在钥匙串里改,不要碰文件。修改后打开一个新终端,加载脚本就会获取到新值。
- 永远不要直接把密钥粘贴到命令里。始终使用交互式输入。内联输入的密钥会永久留在 Shell 历史中。
~/.api-keys.sh加载脚本里只有密钥名称,所以可以放心提交到你的 dotfiles 仓库。密钥留在每台电脑本地的钥匙串里。
就这样。花二十分钟设置好,你的 API 密钥就不会再泄露到那些你忘记的文件里了。你未来的自己——那个差点把密钥 force push 到公开仓库的自己——会感谢你的。
如果你觉得这篇文章有用,我会写一些关于工程实践和开发工具的内容。欢迎在评论区留言,聊聊你是怎么处理本地密钥的,我很好奇大家最终都用了什么方案。
