Python中如何优雅的实现代码与敏感信息分离?
昨天晚上十一点多吧,我在公司楼下抽烟,手机“叮”一下,群里小李说:哥,线上又 401 了,第三方接口全挂……我第一反应不是去看接口,是去看谁又把 key 写代码里了,你们懂吧,这种事就跟“把家门钥匙贴门上”一样,平时没事,一出事就社死。
我后来翻了一眼仓库提交记录,果然有个同学为了赶进度,把 API_KEY="xxxx" 直接塞进了 client.py,然后日志里还把请求头打印出来了,哎哟我当时就…算了不骂人。反正这个话题就很现实:代码要进 git、要 review、要发包、要被别人看;敏感信息呢,最好像银行卡密码一样,别跟代码搅一块。
你问“优雅”怎么搞,我一般不讲那些虚的,先把底线立住:代码仓库里永远不出现真实密钥,出现也得是“示例/占位/模板”。然后运行时再把密钥“注入”进去,注入的方式按环境来:本地用 .env,线上用环境变量/容器 secret/云厂商密钥服务。你要是还在代码里拼字符串,那迟早还得再来楼下抽烟。
先给你们看一个我常用的小骨架,特别土但好用:一个 Settings 统一拿配置,业务代码只跟 settings.xxx 打交道。这样你想换注入方式,不动业务逻辑。
# settings.py
from __future__ import annotations
from dataclasses import dataclass
import os
def_must(name: str) -> str:
v = os.getenv(name)
ifnot v:
raise RuntimeError(f"缺配置:{name}(别在代码里写,去环境变量/secret 配)")
return v
def_get(name: str, default: str) -> str:
v = os.getenv(name)
return v if v isnotNoneelse default
@dataclass(frozen=True)
classSettings:
env: str
db_dsn: str
third_api_key: str
third_base_url: str
log_level: str = "INFO"
defload_settings() -> Settings:
env = _get("APP_ENV", "dev")
# 这里的命名我喜欢加前缀,省得跟系统变量打架
return Settings(
env=env,
db_dsn=_must("APP_DB_DSN"),
third_api_key=_must("APP_THIRD_API_KEY"),
third_base_url=_get("APP_THIRD_BASE_URL", "https://api.example.com"),
log_level=_get("APP_LOG_LEVEL", "INFO"),
)
然后业务代码别再到处 os.getenv,统一从入口加载一次,别搞得像菜市场砍价一样到处问。
# app.py
from settings import load_settings
settings = load_settings()
defcall_third_party(payload: dict) -> dict:
# 注意:别把 key 打日志里,别手滑
headers = {"Authorization": f"Bearer {settings.third_api_key}"}
# ... 你这里 requests 发出去就行
return {"ok": True}
你看这个方式“优雅”的点在哪:敏感信息的“获取”跟“使用”分开。使用的人不需要知道 key 从哪来,反正 settings 有;运维/部署的人也不需要改代码,反正我给你留变量名了。
那本地开发咋办,总不能让大家都手动 export 一堆变量吧,烦死。我们一般用 .env,但注意啊:.env 只在本地,**必须进 .gitignore**,然后仓库里放一个 .env.example(只有占位和说明)。
我不爱引太多依赖,但为了省事,你也可以用 python-dotenv。不想装库也行,写个 20 行解析器就够了(别笑,我真干过)。
# dotenv_loader.py
from __future__ import annotations
defload_dotenv(path: str = ".env") -> None:
try:
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
ifnot line or line.startswith("#"):
continue
if"="notin line:
continue
k, v = line.split("=", 1)
k = k.strip()
v = v.strip().strip('"').strip("'")
# 不覆盖已有环境变量,避免你线上误加载本地值
import os
os.environ.setdefault(k, v)
except FileNotFoundError:
pass
入口这样用一下就行,别在业务模块里乱用:
# main.py
from dotenv_loader import load_dotenv
from settings import load_settings
load_dotenv(".env") # 本地有就读,没有就算了
settings = load_settings()
print("env =", settings.env)
说到线上,很多人卡在“我到底把 secret 放哪”。你要是 Docker / K8s,我建议别发明新轮子:容器 Secret -> 环境变量最省脑子。K8s Secret mount 成 env 或文件都行,但我个人更喜欢 env,因为 settings 这一套不用改。当然如果你们公司要求文件挂载,也行,把 _must() 换成优先读文件再读 env 就完事,别把复杂度扔给业务代码。
还有个坑我必须插一句,前两周我们组就踩了:配置校验要“启动即失败”。别等到跑到一半才发现没 key,那种报警能把人吓醒。所以上面我用 _must(),没配置直接 raise,宁可启动失败,也别半夜 401。
再碎一句,很多泄漏不是“写在代码里”,而是“写在日志里”。有人喜欢 debug 时 print(headers),然后日志采集一上来,密钥就跟着进 ES 了,救命。你可以搞个很土的脱敏函数,打日志统一过它,别靠自觉。
# safe_log.py
defmask(s: str, keep: int = 4) -> str:
if s isNone:
return""
if len(s) <= keep:
return"*" * len(s)
return s[:keep] + "*" * (len(s) - keep)
deflog_request(api_key: str, url: str) -> None:
# 只留前几位,够你排查“是不是用错 key”,又不会全暴露
print(f"calling {url}, key={mask(api_key)}")
最后我随口讲个“团队层面”的小习惯哈,我们后来加了个提交前检查,见到 API_KEY=、SECRET= 这种就直接拦,省得 review 漏掉。你别指望人永远不犯错,架构师干的事就是让“犯错成本变高、事故概率变低”,对吧。
行了我说着说着又跑题了,刚才烟都抽完了…哦对,群里还在喊我去看数据库慢查询,等会我上去看看,先这样吧。