加密字段需继承Field重写from_db_value/to_python/get_prep_value方法,用Fernet实现自动加解密,密文长度至少为原文2倍,禁止加密可搜索字段,密钥须安全存储且不硬编码。

加密字段必须绕过 Django 默认序列化流程
Django 的 Model.save() 和查询过程会直接操作数据库字段值,如果在 save() 里手动调用加密逻辑,但没拦截 __get__/__set__,读取时拿到的就是密文——用户看到的是乱码,不是解密后的内容。核心在于:加密/解密动作必须发生在字段值进出 Python 对象的瞬间,而不是仅在存库时触发。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 继承
Field,重写from_db_value()(从数据库读取后解密)和to_python()(反序列化时也解密) -
get_prep_value()用于存库前加密,确保传给数据库的是密文 - 避免在
save()中做加解密——它不控制字段级数据流转,容易漏掉bulk_create、update()等场景 - 如果用
django.contrib.postgres.fields.JSONField这类复合类型,加密需作用于整个 JSON 字符串,不能只加密某个 key
对称加密选 Fernet 而不是 AES 手动实现
自己拼 AES + PKCS7 + IV + HMAC 容易出错,且 Django 字段生命周期里难以安全传递 IV 或 salt。而 Fernet 是封装好的对称加密协议,自带时间戳、完整性校验和密钥派生,一次调用完成加解密,失败直接抛 CryptographyError,适合字段级封装。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 用
from cryptography.fernet import Fernet,密钥存在环境变量或settings.SECRET_KEY_ENCRYPTION中,**不要硬编码** -
Fernet只接受bytes,字段值是字符串时要统一用.encode('utf-8')和.decode('utf-8') - 密钥长度必须是 32 字节,可用
Fernet.generate_key()生成,再 base64 编码后存配置 - 别用
pycryptodome的AES.new()—— 它不自动处理 IV 保存/恢复,在字段 get/set 中极易导致解密失败
CharField 存密文时长度要预留足够空间
Fernet 加密后输出是 base64 字符串,比原文长约 36%;加上可能的 padding 和元数据,100 字符原文加密后可能达 150+ 字符。若原字段是 CharField(max_length=100),加密后必然截断,存库时报 DataTooLong 或静默丢数据。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 密文字段的
max_length至少设为原文最大长度 × 2,比如原文最多 200 字符,密文字段至少max_length=500 - 如果字段允许空值,注意
None和空字符串加密行为不同:Fernet不接受None,需在get_prep_value()中显式转成b''或跳过加密 - 迁移已有明文数据时,不能直接
makemigrations—— 要写自定义迁移,在RunPython中逐条读取、加密、保存
搜索和过滤会失效,除非改用数据库端加密或放弃模糊匹配
加密后字段在数据库里是随机字符串,filter(name__icontains='admin') 这种查询完全无效。Django 不可能把密文再解密后做 LIKE 匹配,那等于把密钥暴露给 DB 进程,也违背加密本意。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 需要搜索的字段(如用户名、邮箱)**不要加密**;敏感字段如身份证号、银行卡号才加密
- 真有搜索需求,可额外建一个
sha256(明文+salt)的哈希字段用于等值匹配,但无法支持模糊、范围查询 - 别试图用
extra(where=...)或原生 SQL 在数据库里解密——多数数据库不内置 Fernet 支持,且密钥管理失控 - 使用
select_related或prefetch_related时,加密字段仍按需解密,性能影响小;但values_list('encrypted_field')返回的是密文,除非你重写了value_to_string
bulk_update 绕过 save(),model_to_dict 不走 __get__,这些地方漏掉加解密,数据就彻底错位了。


















