
密钥硬编码在代码里,推到 GitHub 上就等于公开。环境变量是最简单的解法,代码里只写变量名,值留在运行环境里。
例如,我们可以用export的方式将密钥写入环境变量:
export JWT_SECRET_KEY=$(openssl rand -hex 32)
短短一行,涉及三个独立的知识点:export 是什么、openssl rand 在干嘛、$(...) 又是什么语法。这篇把它们逐个拆开,再讲讲 Python 项目里管理密钥的常见做法。
一、先拆命令:这一行到底做了什么
export JWT_SECRET_KEY=$(openssl rand -hex 32)
从内往外拆,分三层:
1.1 第一层:openssl rand -hex 32
openssl 是一个加密工具箱,几乎所有 Linux/macOS 系统都预装了。rand 是它的子命令,作用是生成密码学安全的随机数据。
openssl rand -hex 32
# a3f1b9c7e2d4...(64个十六进制字符)
-hex表示输出格式为十六进制(0-9, a-f)32是字节数,不是字符数
32 字节 = 256 bit 的随机数据,用 hex 表示就是 64 个字符。为什么选 256 bit?因为 JWT 默认的 HS256 算法名字里的 256 就是指 256 bit 的密钥强度,32 字节刚好匹配。
其他常见的输出格式:
openssl rand -hex 16 # 128 bit,32 个十六进制字符
openssl rand -hex 32 # 256 bit,64 个十六进制字符
openssl rand -base64 32 # 32 字节,用 Base64 编码输出(更短)
关键点:这里生成的是密码学安全的随机数,不是 random.random() 那种伪随机。密码学安全意味着攻击者无法通过观察输出来预测下一个值,适合用来做密钥、Token、Session ID 这类安全敏感的场景。
1.2 第二层:$(…) 命令替换
$(...) 是 Shell 的命令替换(command substitution)语法。它会先执行括号里的命令,然后把输出结果填回原位。
# 这两行效果一样:
JWT_SECRET_KEY=$(openssl rand -hex 32)
# 等价于手动复制粘贴:
JWT_SECRET_KEY=a3f1b9c7e2d4...
Shell 看到 $(openssl rand -hex 32) 时,会先跑 openssl rand -hex 32,拿到输出的 64 个字符,然后把这串字符填到 = 右边,赋值给变量。
你在其他地方也会看到这个语法:
CURRENT_DATE=$(date +%Y-%m-%d) # 拿到今天的日期
FILE_COUNT=$(ls | wc -l) # 拿到当前目录的文件数
GIT_HASH=$(git rev-parse HEAD) # 拿到当前 commit 的哈希
1.3 第三层:export
export 把变量标记为环境变量,让子进程也能看到它。
(1) 不加export,只是shell变量
没有 export,只是 Shell 变量。当前Shell能用,启动的程序看不到。
JWT_SECRET_KEY=abc123
python app.py # app.py 里 os.environ["JWT_SECRET_KEY"] → KeyError
(2) 使用export定义为环境变量
使用export定义为环境变量,子进程也能读到。
export JWT_SECRET_KEY=abc123
python app.py # app.py 里 os.environ["JWT_SECRET_KEY"] → 密钥内容
这就是为什么必须加 export。不加的话,python app.py 是当前 Shell 启动的子进程,它读不到这个变量。
1.4 常见踩坑:export 了但 Python 还是 KeyError
加了 export,运行 Python 还是报 KeyError: 'JWT_SECRET_KEY'。十有八九是 export 和 python 不在同一个终端会话里。
export 设的环境变量只在当前终端有效。换一个终端窗口、新开一个 tab、关掉终端重新打开,之前 export 的变量全没了。验证方法很简单:
# 在同一个终端里,先 export 再 echo 确认,最后跑 Python
export JWT_SECRET_KEY=$(openssl rand -hex 32)
echo $JWT_SECRET_KEY # 能打印出来才说明 export 成功了
python app.py # 紧接着跑,不要切窗口
几种典型的"不在同一个会话"的情况:
| 你做了什么 | 为什么读不到 | 怎么解决 |
|---|---|---|
| 终端 A 里 export,终端 B 里跑 Python | 不同终端是不同的 Shell 进程,变量不共享 | 在同一个终端里操作 |
| 终端里 export,但从 VS Code / PyCharm 点"运行" | IDE 启动 Python 用的是自己的终端环境,不是你手动操作的那个 | 在 IDE 内置终端里重新 export,或用 .env 文件 |
| export 之后关了终端,重新打开再跑 | export 不会持久化,终端关了就没了 | 每次开终端重新 export,或用 .env 文件 |
写在 ~/.bashrc 里但没 source | 修改配置文件后需要 source ~/.bashrc 才会生效 | 执行 source ~/.bashrc 或重新打开终端 |
如果你经常遇到这个问题,说明 export 的方式不适合你的工作流。 后面会讲 .env 文件的方案,把变量写在项目里,Python 启动时自动加载,不依赖终端状态。
二、环境变量是什么
理解了命令,再往上一层看:环境变量到底是个什么东西?

环境变量是操作系统级别的键值对,从系统 → Shell → 程序逐层继承。export 让变量能穿透到下一层。
环境变量就是操作系统维护的一组键值对,每个进程启动时都会从父进程继承一份。你最常见的环境变量是 PATH,它告诉系统去哪些目录找可执行文件。
# 查看所有环境变量
env
# 查看某一个
echo $PATH
echo $HOME
你自己 export 的变量和系统的 PATH、HOME 本质上没有区别,都是键值对,只是名字和用途不同。
2.1 Python 里怎么读
import os
# 读取,不存在就 KeyError
secret = os.environ["JWT_SECRET_KEY"]
# 读取,不存在就返回默认值
debug = os.environ.get("DEBUG", "false")
两种写法的选择很有讲究:
os.environ["KEY"]:不存在就崩。适合必须有的配置,比如密钥。宁可启动时崩掉,也不要用默认值悄悄跑,密钥用默认值上线就是安全事故。os.environ.get("KEY", default):不存在就用默认值。适合可选配置,比如DEBUG、PORT。
2.2 os.environ也能写,但别用它管密钥
os.environ 本质就是一个字典,能读当然也能写:
import os
import secrets
os.environ["JWT_SECRET_KEY"] = secrets.token_hex(32)
print(os.environ["JWT_SECRET_KEY"]) # 后面的代码确实能读到
但这个值只活在当前 Python 进程里。进程一退出就没了,不会影响终端环境,也不会影响其他程序。
-
和
export的区别:export(Shell)os.environ["X"] = "Y"(Python)生效范围 当前终端 + 所有子进程 当前 Python 进程 进程退出后 终端关闭前都在 没了 密钥来源 外部注入,代码里看不到值 写在代码里,和硬编码没区别 所以正式代码里不要用
os.environ写密钥。那只是换了种方式把密钥写在了源码里。它更常见的用途是测试,临时注入一个假值方便跑测试用例:# test_auth.py import os os.environ["JWT_SECRET_KEY"] = "test-key-not-for-production" # 然后运行被测代码...
三、密钥放在哪里
3.1 为什么不能把密钥写在代码里
新手最自然的写法是这样:
SECRET_KEY = "my-super-secret-key-123"
能跑,但有三个问题:
1. 推到 GitHub 就公开了。 代码仓库通常是团队共享的,甚至是开源的。密钥一旦进了 git 历史,即使后来删掉,用 git log 也能找回来。GitHub 上有专门的机器人扫描泄露的密钥,AWS 的 Access Key 泄露后平均几分钟就会被人拿去挖矿。
2. 不同环境用不同的值。 开发环境、测试环境、生产环境的密钥应该不同。如果写死在代码里,切换环境就要改代码,改了又容易忘记改回来。
3. 轮换密钥要改代码。 密钥泄露或定期轮换时,如果密钥在代码里,就得改代码、提交、部署。如果在环境变量里,改个配置重启就行,不需要动代码。
用环境变量解决这些问题的思路很简单:代码里只写变量名,值留在运行环境里。 代码可以公开,只要环境变量不泄露,密钥就是安全的。
3.2 .env 文件:开发环境的便捷方案
每次开终端都要手动 export 一堆变量很烦。开发阶段常见的做法是把变量写在项目根目录的 .env 文件里:
# .env
JWT_SECRET_KEY=a3f1b9c7e2d4a8f6b1c3d5e7f9a0b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f2
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
DEBUG=true
然后在 Python 里用 python-dotenv 自动加载:
pip install python-dotenv
from dotenv import load_dotenv
load_dotenv() # 读取 .env 文件,把里面的键值对注入环境变量
import os
secret = os.environ["JWT_SECRET_KEY"] # 现在能读到了
load_dotenv() 做的事很简单:读 .env 文件的每一行,按 KEY=VALUE 格式解析,然后调用 os.environ 设置进去。之后的代码用 os.environ 读取,和真正的环境变量完全一样。
3.3 .env 绝对不能提交到 git
这是最容易犯的错。.env 里存的就是密钥,提交了等于白做。项目初始化时就要把它加进 .gitignore:
echo ".env" >> .gitignore
那新同事怎么知道要设哪些变量?常见做法是提供一个 .env.example 文件:
# .env.example(提交到 git,告诉别人需要哪些变量)
JWT_SECRET_KEY=<用 openssl rand -hex 32 生成>
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
DEBUG=true
.env.example 里写的是说明和示例值,不是真正的密钥。新同事 clone 项目后,复制一份改成自己的值就行:
cp .env.example .env
# 然后编辑 .env,填入真正的值
四、生产环境怎么管理
.env 文件适合开发,但生产环境有更专业的方案:
| 方案 | 适合场景 | 说明 |
|---|---|---|
| 系统环境变量 | 单机部署 | 写在 /etc/environment 或 systemd 的 service 文件里 |
| Docker | 容器化部署 | docker run -e JWT_SECRET_KEY=xxx 或 docker-compose.yml 的 environment 字段 |
| 云平台配置 | 云服务 | AWS Parameter Store、阿里云配置中心、Vercel Environment Variables |
| Vault | 企业级 | HashiCorp Vault,支持自动轮换、审计、细粒度权限 |
不管用哪种方案,核心原则不变:代码里读 os.environ,值由部署环境提供。 代码不关心密钥从哪来,只关心 os.environ["JWT_SECRET_KEY"] 能拿到值。
以Docker Compose 为例,实际项目中,密钥一般也写在 .env 文件里,docker-compose 会自动读取同目录下的 .env,不需要你手动 export。

密钥的流转路径:.env 文件 → Docker Compose 读取并替换占位符 → 注入容器环境变量 → Python 用 os.environ 读取。密钥始终不出现在代码或 YAML 里。
# .env文件内容(和 docker-compose.yml 放在同一目录)
JWT_SECRET_KEY=a3f1b9c7e2d4a8f6b1c3d5e7f9a0b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f2
DATABASE_URL=postgresql://user:pass@db:5432/mydb
# docker-compose.yml
services:
web:
build: .
environment:
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
- DATABASE_URL=${DATABASE_URL}
流程是这样的:docker-compose up 启动时,Compose 先读 .env 文件里的键值对,然后把 ${JWT_SECRET_KEY} 这类占位符替换成实际的值,最后注入到容器内部的环境变量里,容器里的 Python 代码用 os.environ["JWT_SECRET_KEY"] 读取,和本地开发一模一样。
.env 文件同样不能提交到 git,和前面讲的规则一致。团队协作时提供 .env.example 告知需要哪些变量,每个人自己创建 .env 填入真实值。
五、完整的项目结构示例
把上面讲的串起来,一个典型的 Flask 项目应该长这样:
my-project/
├── .env ← 本地开发用,不提交
├── .env.example ← 提交到 git,说明需要哪些变量
├── .gitignore ← 包含 .env
├── app.py
└── requirements.txt
-
.gitignore:.env __pycache__/ -
.env.example:# 密钥:用 openssl rand -hex 32 生成 JWT_SECRET_KEY= # 数据库(本地开发默认值) DATABASE_URL=postgresql://localhost:5432/mydb # 调试模式 DEBUG=true -
app.py:import os from dotenv import load_dotenv load_dotenv() SECRET_KEY = os.environ["JWT_SECRET_KEY"] # 必须有,没有就崩 DEBUG = os.environ.get("DEBUG", "false") == "true" # 可选,默认关闭
六、总结
回到开头那行命令:
export JWT_SECRET_KEY=$(openssl rand -hex 32)
| 部分 | 作用 |
|---|---|
openssl rand -hex 32 | 生成 256 bit 密码学安全的随机密钥 |
$(...) | 命令替换,把输出填到等号右边 |
export | 标记为环境变量,让 Python 子进程能读到 |
背后的原则也就一句话:密钥不进代码,代码只读变量名。 开发用 .env + python-dotenv,生产用平台提供的配置管理。这样代码可以安全地提交、分享、开源,密钥的生命周期和代码的生命周期彻底解耦。
91



被折叠的 条评论
为什么被折叠?



