必须用自定义加密字段+独立环境变量密钥,因save()手动加解密会导致filter失效、Admin乱码、bulk操作明文入库、序列化泄露明文;正确做法是继承models.Field并覆盖get_prep_value、from_db_value、to_python,密钥须严格分离且不可变更。

必须用自定义加密字段 + 独立环境变量密钥,不能在 save() 里手动加解密,也不能复用 SECRET_KEY。
为什么直接在 save() 里加密会出问题
常见错误是重写模型的 save() 方法,在存入前对字段值调用 Fernet.encrypt(),读取时再手动 decrypt()。这会导致:
-
filter(api_key__icontains="abc")返回空——ORM 查询走的是原始字段值,但数据库里存的是密文 - Admin 页面显示乱码或 base64 字符串——
to_python()没被调用,后台直接渲染了加密后的 bytes -
bulk_create()或update()绕过save(),明文直接入库 - 序列化(如 DRF
ModelSerializer)输出明文——因为没经过from_db_value()
怎么正确实现加密字段
继承 models.Field,完整覆盖三个核心方法,并用 cryptography.fernet 封装细节:
-
get_prep_value(self, value):输入 Python 字符串,返回加密后 base64 编码的str(例如Fernet(key).encrypt(b"raw").decode()) -
from_db_value(self, value, expression, connection):从数据库读出 base64 字符串,解密并返回原始字符串 -
to_python(self, value):处理表单提交、JSON 反序列化等场景,同样要解密;若传入已是解密后值,需判断避免重复解密 - 必须设置
description = "Encrypted text field",否则makemigrations可能报错
示例片段(不依赖第三方包):
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
立即学习“Python免费学习笔记(深入)”;
from cryptography.fernet import Fernet
import os
<p>class EncryptedAPIKeyField(models.CharField):
def <strong>init</strong>(self, *args, *<em>kwargs):
kwargs.setdefault('max_length', 512)
super().<strong>init</strong>(</em>args, **kwargs)</p><pre class='brush:python;toolbar:false;'>def get_prep_value(self, value):
if not value:
return value
key = os.environ.get('DJANGO_ENCRYPTION_KEY')
if not key:
raise ValueError("DJANGO_ENCRYPTION_KEY not set")
f = Fernet(key.encode())
return f.encrypt(value.encode()).decode()
def from_db_value(self, value, expression, connection):
if not value:
return value
key = os.environ.get('DJANGO_ENCRYPTION_KEY')
f = Fernet(key.encode())
return f.decrypt(value.encode()).decode()
def to_python(self, value):
if isinstance(value, str) and len(value) > 50: # 判定是否为密文(base64长度特征)
return self.from_db_value(value, None, None)
return value
description = "Encrypted text field"密钥管理的硬性要求
DJANGO_ENCRYPTION_KEY 必须与 SECRET_KEY 严格分离,且满足:
- 必须是 32 字节密钥经
base64.urlsafe_b64encode()编码后的字符串(如YQsZz7VxKj9LmNpRtWvXyZaBcDeFgHiJkLmNoPqRsTuVwXyZaBcD==) - 生产环境只能从环境变量读取,绝不能写死、不能从
SECRET_KEY衍生(除非用pbkdf2_hmac('sha256', SECRET_KEY.encode(), b'django-fernet-v1', 100_000, dklen=32)) - .env 文件必须进
.gitignore;Docker 中优先用secrets挂载,而非env_file - 密钥一旦上线,全生命周期不可变更——否则存量数据永久无法解密
查询和搜索的现实限制
加密字段天然不支持 __icontains、__startswith 等模糊查询。如果业务需要按 API 密钥前缀查用户,得额外加辅助字段:
- 新增
search_hash = models.CharField(max_length=64, db_index=True) - 写入时同步计算:
hashlib.sha256(key.strip().lower().encode()).hexdigest() - 查询改用
filter(search_hash__exact=...),注意这个哈希仅用于等值匹配,不可逆
真正难的不是加解密逻辑本身,而是密钥生命周期管理、ORM 各环节钩子的全覆盖、以及接受“加密即放弃模糊查询”这个前提——很多团队卡在这一步,硬上 LIKE 查询反而把密钥暴露在日志或慢查询中。

















