从硬编码到安全存储:Python密钥管理最佳实践
发布时间:2026/9/3 21:12:59 锦皓数字建站

在 Python 项目里处理 API Key、Token、数据库密码这类敏感信息时很多新手甚至部分有经验的开发者都会习惯性地把 Key 直接写在代码文件里。这种现象在 GitHub、Gitee 上非常常见往往一个git push密钥就跟着源码一起泄露了。轻则账号被盗刷重则云服务器被挖矿、数据库被删库勒索。本文就围绕“Python 的 Key 不要写在代码里”这一主题整理一套从认识到落地的完整方案包含环境变量、.env文件、加密配置、密钥管理服务等多层安全设计思路并通过一个完整可运行的 Python 实战项目演示如何一步步去掉硬编码 Key。不管是写爬虫、调 OpenAI 接口、接入支付宝支付还是做企业后端服务这套方法都适用。1. 为什么 Python 项目的 Key 不能写在代码里1.1 硬编码 Key 的真实风险先看一个最简单的错误示例。很多人的代码是这样写的API_KEY sk-1234567890abcdef API_SECRET my_secret_key_2024 DB_PASSWORD root123456乍一看很省事运行时直接读取变量就能用。但问题是这份代码一旦被提交到 Git 仓库就会被永久记录在历史提交中。即使你之后把 Key 删掉再提交一次攻击者依然可以翻看提交历史拿到原始 Key。硬编码 Key 至少带来以下几类风险密钥泄露范围不可控。代码仓库、压缩包、网盘分享、技术文章截图都可能成为泄露渠道。权限无法单独回收。修改一次密钥需要重新发布整个项目影响面大。审计困难。分不清某个 Key 是哪位同事在哪个环境使用的出了问题难以追溯。容易被自动化工具扫到。GitHub 有专门的密钥扫描机器人攻击者也会用爬虫搜索API_KEY 、password 等特征字符串。你可能会想“我的项目是私有的不会泄露。”但私有仓库同样存在风险比如员工离职带走代码、第三方外包人员接触源码、本地开发环境被植入恶意软件等。密钥管理的核心原则就是永远假设代码最终会被不该看到的人看到。1.2 常见泄露场景复盘在实际开发中我见过或处理过的密钥泄露场景大致有以下几类场景一把 Key 写进代码后推到 GitHub这是最常见的泄露方式。很多人在本地写完代码顺手提交到远程仓库里面带着真实的 API Key。几个小时后攻击者通过扫描机器人拿到 Key开始调用付费 API账单直接飙到几千元。场景二前端项目中硬编码后端接口密钥在一些管理后台项目中为了省事直接把后端服务的 AccessKey 写在 JavaScript 代码里。前端代码无法真正保密任何人按 F12 都能看到所有请求参数Key 自然也就暴露了。场景三测试环境与生产环境使用同一把 Key为了省事开发、测试、生产共用一把数据库密码或云厂商密钥。一旦测试环境被攻破生产环境也跟着遭殃。场景四密钥出现在日志、错误上报、截图里代码里虽然没有硬编码但异常日志里打印了完整请求参数里面包含密钥。日志文件如果被上传到日志平台或第三方分析工具同样会造成泄露。这些场景都指向同一个结论密钥与代码分离、密钥与环境绑定、密钥权限收敛是必须要做好的基础安全工作。1.3 安全设计的基本原则在动手写代码之前先明确几个安全设计原则后续所有方案都围绕它们展开密钥不入库任何环境下的真实密钥都不应该出现在 Git 仓库中。环境隔离开发、测试、生产环境使用不同的密钥互不影响。最小权限每个密钥只赋予完成任务所需的最小权限比如只读权限就不给写权限。可轮换密钥需要支持定期更换更换过程应尽可能自动化。可审计密钥的使用者、使用时间、调用来源应能被记录和追踪。理解了这些原则下面就可以开始选择具体的落地工具了。2. 环境准备与依赖说明2.1 Python 环境要求本文示例主要基于 Python 3。建议使用 Python 3.8 及以上版本因为os.environ、pathlib、dataclasses等标准库在更高版本中行为更稳定。python --version如果还没有安装 Python可以到 Python 官网下载对应操作系统的安装包。Windows 用户安装时务必勾选Add Python to PATH。macOS 用户也可以通过 Homebrew 安装。2.2 需要用到的库本文会用到以下 Python 库python-dotenv读取.env文件中的键值对并写入环境变量。cryptography提供对称加密算法用于加密保存密钥文件。requests发送 HTTP 请求模拟调用第三方 API 的场景。安装命令pip install python-dotenv cryptography requests如果你的项目使用虚拟环境建议先创建并激活虚拟环境再安装python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.3 示例项目结构后面的实战案例会采用如下项目结构secure-key-demo/ ├── .env.example # 环境变量模板文件可提交到 Git ├── .env # 真实环境变量文件禁止提交 ├── .gitignore # 忽略敏感文件 ├── config.py # 配置加载模块 ├── main.py # 主程序入口 ├── encrypt_key.py # 密钥加密工具 ├── decrypt_usage.py # 解密使用示例 └── requirements.txt # 依赖清单3. 安全存储 Key 的几种主流方案3.1 环境变量方案环境变量是操作系统级别提供的键值对存储机制。Python 可以通过os.environ直接读取。import os api_key os.environ.get(OPENAI_API_KEY) if not api_key: raise RuntimeError(请在环境变量中设置 OPENAI_API_KEY)这种方式的好处是不依赖任何第三方库属于 Python 标准用法。密钥不进入代码文件也不进入 Git。部署到服务器时可以在系统服务配置中设置如 systemd、supervisor、Docker 环境变量。缺点是多环境管理不便。开发环境、测试环境、生产环境需要分别设置变量。本地开发时每次切换终端都需要重新 export比较麻烦。3.2 .env 文件方案.env文件是当前项目级环境配置的主流方案。它把密钥保存在项目根目录的.env文件中然后通过python-dotenv在程序启动时加载到环境变量里。.env文件内容示例# OpenAI 密钥 OPENAI_API_KEYsk-your-key-here # 数据库配置 DB_HOST127.0.0.1 DB_PORT3306 DB_USERroot DB_PASSWORDyour-db-password # 第三方支付 PAYMENT_APP_IDwx1234567890 PAYMENT_SECRETabcdef123456Python 加载代码from dotenv import load_dotenv load_dotenv() # 默认加载当前目录下的 .env 文件 import os api_key os.environ.get(OPENAI_API_KEY)关键点真实.env文件必须加入.gitignore只把.env.example模板提交到仓库。.env.example里的 Value 使用占位符不给真实值。3.3 配置文件方案如果你的项目更习惯使用config.ini、config.yaml这类配置文件也可以将密钥放在配置文件中但要注意区分两类配置非敏感配置如超时时间、重试次数、日志级别可以入库。敏感配置如密码、Token、私钥不要直接写文件或者写入加密后的密文。一个典型的config.yamlapp: name: secure-key-demo debug: true request_timeout: 30 database: host: 127.0.0.1 port: 3306 user: root # password: 不放在这里其中database.password读取时仍然从环境变量或密钥管理服务获取。3.4 加密密钥文件方案如果因为某些原因必须把密钥保存为文件那么建议对文件进行加密。比如用cryptography库的 Fernet 对称加密算法将真实密钥加密后存储为密文文件解密密码单独放在环境变量中。加密后的密钥文件即便被泄露攻击者没有主密码也无法还原原文提高了泄露成本。3.5 密钥管理服务方案在生产环境中更推荐的方案是使用专业的密钥管理服务例如HashiCorp VaultAWS Secrets ManagerAzure Key Vault阿里云 KMS 密钥管理服务这些服务的共同特点是密钥集中存储支持访问控制。支持自动轮换。提供完整审计日志。应用运行时通过 SDK 或 HTTP API 动态获取密钥。Python 中调用 Vault 的简单示例思路如下import hvac client hvac.Client(urlhttps://vault.example.com, tokenos.environ[VAULT_TOKEN]) secret client.secrets.kv.v2.read_secret_version(pathmyapp/database) db_password secret[data][data][password]需要注意这类服务通常需要额外的运维基础设施中小企业或个人开发者用得不多但思路值得了解。4. 完整实战从硬编码到安全存储这一节我们通过一个完整的示例项目演示如何把硬编码的 Key 改成安全的加载方式。项目模拟一个调用第三方 AI 接口获取文本摘要的场景涉及 API Key、数据库密码等敏感信息。4.1 创建项目结构首先创建项目目录mkdir secure-key-demo cd secure-key-demo然后按上文的结构创建文件。我们将在本地完成整个演示。4.2 编写 requirements.txtpython-dotenv1.0.1 cryptography42.0.5 requests2.31.0如果安装时报错可以去掉版本号让 pip 自动选择兼容版本。生产环境建议锁定版本便于复现。4.3 编写 .env.example.env.example是可以提交到仓库的模板文件所有人拿着这个文件复制一份填入自己的真实值即可。# 复制本文件为 .env 并填入真实值 # cp .env.example .env # AI 服务 AI_API_KEYyour-ai-api-key-here AI_BASE_URLhttps://api.example.com AI_MODELgpt-3.5-turbo # 数据库 DB_HOST127.0.0.1 DB_PORT3306 DB_USERroot DB_PASSWORDyour-db-password # 本地加密主密码用于解密 encrypted_key.bin LOCAL_MASTER_KEYyour-local-master-key4.4 编写 .gitignore这一步是最容易忽略的但也是最重要的。在项目一开始就要把敏感文件加进去# 环境变量 .env *.env # 加密密钥文件 *.bin *.key *.pem # Python __pycache__/ *.py[cod] venv/ .venv/ # IDE .idea/ .vscode/ *.swp注意.env.example不应该被忽略因为它不包含真实密钥。.bin文件是否忽略取决于你的使用场景如果密文文件本身也需要入库备份可以视情况处理但绝对不建议把加密主密码一起入库。4.5 编写 config.py 配置加载模块config.py负责统一加载.env文件并提供全局配置对象。这样其他模块只需要from config import settings不需要关心密钥到底从哪里来。# 文件路径secure-key-demo/config.py import os from pathlib import Path from dotenv import load_dotenv # 加载项目根目录下的 .env 文件 BASE_DIR Path(__file__).resolve().parent load_dotenv(BASE_DIR / .env) class Settings: 应用配置类统一读取环境变量中的敏感信息。 def __init__(self): # AI 服务配置 self.ai_api_key self._get_env(AI_API_KEY) self.ai_base_url self._get_env(AI_BASE_URL, https://api.example.com) self.ai_model self._get_env(AI_MODEL, gpt-3.5-turbo) # 数据库配置 self.db_host self._get_env(DB_HOST, 127.0.0.1) self.db_port self._get_env(DB_PORT, 3306) self.db_user self._get_env(DB_USER, root) self.db_password self._get_env(DB_PASSWORD) # 本地加密主密码生产环境建议从密钥管理服务获取 self.local_master_key self._get_env(LOCAL_MASTER_KEY) staticmethod def _get_env(key: str, default: str | None None) - str: 读取环境变量缺失时根据场景决定是否抛异常。 value os.environ.get(key) if value is None or value.strip() : if default is not None: return default raise RuntimeError(f缺少必要环境变量: {key}) return value.strip() settings Settings()在这段代码中_get_env方法对缺失的必填变量直接抛出异常。这样设计的好处是启动阶段就能快速发现问题而不是等到调用第三方 API 时才报错。4.6 编写调用第三方 API 的代码main.py模拟一个调用 AI 服务的入口。它只负责从config.settings中读取密钥不在代码中出现任何真实 Key。# 文件路径secure-key-demo/main.py import requests from config import settings def summarize_text(text: str) - str: 调用 AI 服务返回文本摘要。 这里使用 requests 库实际项目中也可以换成 openai SDK 等。 headers { Authorization: fBearer {settings.ai_api_key}, Content-Type: application/json, } payload { model: settings.ai_model, messages: [ {role: user, content: f请对以下内容进行摘要\n{text}} ], max_tokens: 50, } url f{settings.ai_base_url}/v1/chat/completions response requests.post(url, headersheaders, jsonpayload, timeout30) response.raise_for_status() data response.json() return data[choices][0][message][content] def get_database_connection_info() - dict: 返回数据库连接信息实际项目中会传给 SQLAlchemy 或 pymysql。 这里只演示从配置读取不真正建立连接。 return { host: settings.db_host, port: settings.db_port, user: settings.db_user, password: settings.db_password, } if __name__ __main__: # 验证配置是否正常加载 print(AI Base URL:, settings.ai_base_url) print(DB Host:, settings.db_host) # 实际调用示例这里因为网络原因默认不真正执行 # result summarize_text(Python 密钥管理最佳实践) # print(摘要结果:, result) db_info get_database_connection_info() # 注意不要打印真实密码 print(DB User:, db_info[user]) print(DB Password 长度:, len(db_info[password]))从这份代码可以看到密钥字段在业务代码中完全以配置变量形式出现没有硬编码字符串。即使代码被提交攻击者也拿不到真实值。4.7 编写密钥加密与解密示例有些场景下密钥会以文件形式分发到多台服务器比如离线打包的私钥文件。这时可以用cryptography对密钥文件加密。encrypt_key.py用于生成加密后的密钥文件# 文件路径secure-key-demo/encrypt_key.py from pathlib import Path from cryptography.fernet import Fernet from config import settings def encrypt_secret_to_file(plain_text: str, output_file: str) - None: 使用主密码派生密钥对明文进行加密写入文件。 主密码来源于环境变量 LOCAL_MASTER_KEY。 master_key settings.local_master_key.encode() # Fernet 要求 32 字节 url-safe base64 编码的密钥 # 这里通过 SHA-256 做简单派生确保长度满足要求。 import base64 import hashlib digest hashlib.sha256(master_key).digest() key base64.urlsafe_b64encode(digest) cipher Fernet(key) token cipher.encrypt(plain_text.encode()) output_path Path(output_file) output_path.write_bytes(token) print(f加密完成密文已写入: {output_path.resolve()}) if __name__ __main__: # 示例加密一段数据库密码 secret_value MyRealDatabasePassword123! encrypt_secret_to_file(secret_value, encrypted_key.bin)decrypt_usage.py用于读取密文并解密# 文件路径secure-key-demo/decrypt_usage.py import base64 import hashlib from pathlib import Path from cryptography.fernet import Fernet from config import settings def decrypt_secret_from_file(input_file: str) - str: 从密文文件中解密出原始字符串。 master_key settings.local_master_key.encode() digest hashlib.sha256(master_key).digest() key base64.urlsafe_b64encode(digest) cipher Fernet(key) encrypted_data Path(input_file).read_bytes() decrypted cipher.decrypt(encrypted_data) return decrypted.decode() if __name__ __main__: secret decrypt_secret_from_file(encrypted_key.bin) print(解密成功内容长度:, len(secret)) # 实际使用中不要直接打印原文这里仅演示解密流程 # print(原文:, secret)这段代码的完整性体现在两个脚本可以独立运行。加密主密码不写入文件而是依然从环境变量读取实现“加密文件泄露 没有主密码 无法还原”的效果。4.8 运行与验证先复制.env.example为.env填入实际值cp .env.example .env然后运行主程序python main.py预期输出类似AI Base URL: https://api.example.com DB Host: 127.0.0.1 DB User: root DB Password 长度: 24接着运行加密脚本python encrypt_key.py python decrypt_usage.py预期输出加密完成密文已写入: /path/to/secure-key-demo/encrypted_key.bin 解密成功内容长度: 30至此整个流程跑通了。你会发现代码里没有任何一处真实密钥全部来自.env文件或加密文件。5. 常见问题与排查思路在实际使用中下面这些问题出现的频率比较高。5.1 .env 文件没生效问题现象常见原因解决思路启动代码后settings.ai_api_key为None当前目录不是.env所在目录在config.py中使用绝对路径BASE_DIR / .env加载修改.env后运行结果不变Python 进程缓存了旧环境变量重启 Python 进程或者在代码中显式调用load_dotenv(overrideTrue)线上部署时.env不存在部署流程未复制环境变量检查服务器环境确保 CI/CD 正确注入环境变量5.2 环境变量读取为 None如果使用os.environ.get(KEY)返回None可以按顺序检查是否先调用了load_dotenv()。Key 名称是否拼写正确区分大小写。终端当前工作目录是否包含.env文件。是否在子进程中启动 Python而环境变量只设置在了当前 shell。5.3 配置文件路径找不到当项目被包成可执行文件或部署到 Docker 时__file__的路径可能会发生变化。建议始终使用pathlib.Path(__file__).resolve().parent定位项目根目录不要依赖相对路径。Docker 部署时更推荐直接在docker run或docker-compose.yml中通过environment注入环境变量version: 3.8 services: app: image: my-python-app:latest environment: - AI_API_KEY${AI_API_KEY} - DB_HOSTmysql - DB_PASSWORD${DB_PASSWORD}5.4 加密库安装失败cryptography在某些系统上需要编译依赖安装失败时可以尝试pip install --upgrade pip setuptools wheel pip install cryptographyWindows 用户建议直接使用预编译的 wheel 包通常会自动下载成功。5.5 Git 仓库不小心提交了密钥如果密钥已经被提交到 Git 历史中单纯删除文件并提交是不安全的。你需要立即撤销该密钥在对应的服务商后台重新生成新密钥。使用git filter-repo或 BFG Repo-Cleaner 清理历史记录。通知团队所有成员旧密钥已作废。如果仓库是 GitHub 公开仓库还可以联系 GitHub 支持或使用 GitHub 的密钥扫描提醒功能。6. 工程最佳实践6.1 .gitignore 必须提前配置新建项目的第一个动作就应该是创建.gitignore而不是写业务代码。很多安全事故都发生在项目早期因为那时候大家觉得“只是测试一下”结果就把真实密钥提交上去了。建议在.gitignore中至少包含.env *.pem *.key *.p12 *.jks secrets/6.2 Key 的命名与审计给环境变量命名时建议采用统一的前缀和含义明确的名称# 好的命名 AI_API_KEY DB_PASSWORD ALIYUN_OSS_ACCESS_KEY_ID ALIYUN_OSS_ACCESS_KEY_SECRET # 不推荐的命名 KEY1 SECRET PASSWORD统一的命名便于在代码审计和配置检查中快速定位。如果团队规模较大还应整理一份“敏感配置清单”记录每个 Key 的用途、权限范围、负责人、轮换周期。6.3 最小权限与轮换开发环境使用的密钥尽量不要和生产环境相同。云厂商的控制台通常支持子账号或临时 Token给每个环境创建独立子账号并只授予需要的权限。密钥轮换建议至少每 90 天一次。轮换流程可以设计成新密钥生成旧密钥保留。应用切换到新密钥验证功能正常。确认无误后在服务商后台删除旧密钥。如果你的项目使用 Vault 等工具轮换可以做到自动化进一步降低人工操作带来的风险。6.4 日志与异常信息脱敏日志中不能打印完整密钥。常见做法是只显示前几位和后几位中间用星号替代def mask_secret(secret: str, visible_prefix: int 4, visible_suffix: int 4) - str: 脱敏工具函数将密钥中间部分替换为星号。 if len(secret) visible_prefix visible_suffix: return *** return f{secret[:visible_prefix]}****{secret[-visible_suffix:]} # 示例 api_key sk-1234567890abcdef print(mask_secret(api_key)) # 输出: sk-1****cdef另外使用requests调用 API 时不要将headers整个打印到日志中。在异常处理中也要注意不要把请求体、响应体中的密钥字段写入日志文件。6.5 团队协作中的密钥交接团队成员之间不要在聊天工具中明文发送密钥。可以使用密码管理工具如 1Password、Bitwarden的分享功能或者通过密钥管理服务临时授权。.env.example中的占位符值就是很好的交接媒介——新人只需要复制模板找管理员要真实值填入本地.env。7. 总结Python 项目的密钥管理并不复杂核心就是“分离”二字代码与配置分离配置与真实密钥分离开发环境与生产环境分离。从最基础的os.environ环境变量到.env文件加载再到加密密钥文件和专业的密钥管理服务每一层都在帮你降低密钥泄露带来的损失。这篇文章的实战案例覆盖了一个小型 Python 项目的完整流程通过.env.example提供配置模板。通过.gitignore防止敏感文件入库。通过python-dotenv加载环境变量。通过cryptography对密钥文件加密。这套体系可以满足多数中小型项目的需要。如果你的项目已经上了云服务或处于金融、政务等高安全要求行业可以进一步研究 Vault、KMS 等专业方案。接下来你可以做的练习是把自己之前写过的一个硬编码密钥的小项目按照本文的方案改造一遍并试着在 GitHub 上搜索自己的历史仓库看看有没有泄露过的密钥痕迹。尽早发现问题、尽早处理远比事后补救更省成本。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。