在 Django 模型中安全存储秘密密钥(Fernet 版本)
安全是开发者的责任 – 不能把原文直接存入数据库。
1. 为什么需要加密?
- API Key / Secret / OAuth 令牌 等值,一旦泄露,整个服务就相当于被破解。
- 如果硬编码在
settings.py, → Git 仓库、部署产物、开发者笔记本等地方都会直接暴露。 - 即使放在环境变量中, → 再把该值直接存入数据库,数据库被攻破时就会以明文泄露。
另一个必须注意的点:
- 密码(password)
- 服务器不需要知道原文 → 使用哈希(BCrypt、Argon2)
- API Key / Secret / 令牌
- 服务器需要再次使用原文 → 需要可解密的加密
总结
- 需要再次使用的敏感数据 → 加密后存储
- 不需要再次使用的数据(密码等) → 哈希后存储
2. Fernet 是什么?
cryptography.fernet.Fernet 是一个一次性提供高层 API。
- 内部使用安全算法(AES + HMAC 等)
- 随机 IV、时间戳、完整性校验(HMAC) 等自动处理
- 开发者只需妥善保管密钥,然后
encrypt(明文)→ 令牌字符串decrypt(令牌)→ 明文 只需调用一次即可
换句话说,省去了直接处理 AES-CBC/IV/填充/HMAC 的麻烦:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # 生成密钥
f = Fernet(key)
token = f.encrypt(b"hello") # 加密
plain = f.decrypt(token) # 解密
只要理解到这点,就足以使用。
我们需要关注的只有两件事
- 密钥管理(环境变量、Secret Manager 等)
- 在 Django 中写入/读取时自动加密/解密
3. Fernet 密钥生成与 Django 设置
3-1. 生成 Fernet 密钥
# 生成一个 Fernet 密钥(Base64 字符串)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
示例输出:
twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY=
将此字符串放入服务器环境变量。
export DJANGO_FERNET_KEY="twwhe-3rGfW92V5rvvW3o4Jhr0rVg7x8lQM6zCj6lTY="
3-2. 在 settings.py 中准备 Fernet 实例
# settings.py
import os
from cryptography.fernet import Fernet
DJANGO_FERNET_KEY = os.environ["DJANGO_FERNET_KEY"] # 字符串
FERNET = Fernet(DJANGO_FERNET_KEY.encode("utf-8"))
现在在任何地方都可以这样使用:
from django.conf import settings
settings.FERNET.encrypt(...)
settings.FERNET.decrypt(...)
4. 在 Django 模型中添加 Fernet 加密字段
不需要实现复杂的块加密,只需一个极简的自定义字段即可。
4-1. 设计要点
- 数据库中只存加密后的令牌字符串
- 模型/视图/服务代码始终只看到解密后的明文
- 采用
_secret+secret的模式,将数据库列与业务意义分离
4-2. EncryptedTextField 的实现
# myapp/utils/encrypted_field.py
from django.db import models
from django.conf import settings
class EncryptedTextField(models.TextField):
"""
基于 Fernet 的加密 TextField
- 存储时:明文(str) → Fernet 令牌(str)
- 读取时:Fernet 令牌(str) → 明文(str)
"""
description = "Fernet encrypted text field"
def get_prep_value(self, value):
"""
Python 对象 → 数据库存储值
"""
if value is None:
return None
if isinstance(value, str):
if value == "":
return ""
token = settings.FERNET.encrypt(value.encode("utf-8"))
return token.decode("utf-8")
return value
def from_db_value(self, value, expression, connection):
"""
数据库值 → Python 对象
"""
if value is None:
return None
try:
token = value.encode("utf-8")
decrypted = settings.FERNET.decrypt(token)
return decrypted.decode("utf-8")
except Exception:
return value
def to_python(self, value):
"""
ORM 处理时的 Python 对象
"""
return value
实务提示
- 如果最初数据库里已有明文数据, 在
from_db_value中让解密失败时返回原值, 之后逐步读取 → 再写入时就会变成加密值。
5. 在模型中使用
# myapp/models.py
from django.db import models
from .utils.encrypted_field import EncryptedTextField
class MyModel(models.Model):
name = models.CharField(max_length=100)
_secret = EncryptedTextField() # 实际数据库列(Fernet 令牌)
@property
def secret(self):
"""
对外使用时始终返回明文
"""
return self._secret
@secret.setter
def secret(self, value: str):
"""
赋值时自动加密
"""
self._secret = value
_secret- 真实数据库列名
- 存储的是 Fernet 令牌(加密字符串)
secret- 代码中使用的“逻辑字段”
- 始终只处理明文
这样,后续查看模型时可以清晰区分:
obj.secret # 明文
obj._secret # 加密值
6. 使用示例
6-1. Django shell
>>> from myapp.models import MyModel
# 新建对象
>>> obj = MyModel(name="Test")
>>> obj.secret = "my-very-secret-key" # 赋值明文
>>> obj.save()
# 数据库中实际存储的是加密令牌
>>> MyModel.objects.get(id=obj.id)._secret
'gAAAAABm...4xO8Uu1f0L3K0Ty6fX5y6Zt3_k6D7eE-' # 示例
# 模型使用时得到明文
>>> obj.secret
'my-very-secret-key'
6-2. API 中使用
通常不会把 secret 直接返回给客户端,而是仅在后端内部使用。示例:
# myapp/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import MyModel
class SecretView(APIView):
def get(self, request, pk):
obj = MyModel.objects.get(pk=pk)
# obj.secret 已经是明文(已解密)
return Response({"secret": obj.secret})
在实际项目中,常见做法是:
- 用
obj.secret进行外部 API 调用 - 不将其直接暴露给前端
7. 使用 Fernet 时需要注意的事项
7-1. 搜索/过滤限制
Fernet 的加密结果每次都会不同。
obj1.secret = "abc"
obj2.secret = "abc"
虽然明文相同,但 _secret 存储的令牌完全不同。
因此,以下查询无效:
MyModel.objects.filter(_secret="abc") # 无意义
MyModel.objects.filter(_secret=obj._secret) # 只能按令牌比较,无法按明文搜索
如果需要按明文搜索/排序,建议:
- 另外维护搜索列(哈希、前缀等)
- 或者重新评估是否需要加密该字段
7-2. 密钥轮换策略
不建议一直使用同一密钥,最好定期更换。
- 简单做法:
- 只用
DJANGO_FERNET_KEY - 进阶做法:
- 维护
OLD_KEY、NEW_KEY - 加密时仅用 NEW_KEY
- 解密时尝试 OLD_KEY 与 NEW_KEY
- 随着数据逐步迁移到 NEW_KEY,最终移除 OLD_KEY
此部分可在单独文章中详细展开。
8. 安全检查清单(Fernet 版本)
| 项目 | 检查点 |
|---|---|
| 密钥管理 | DJANGO_FERNET_KEY 不要放到 Git,使用 AWS Secrets Manager / Parameter Store / Vault 等 |
| 权限分离 | 仅在生产服务器注入密钥,开发/本地使用不同密钥 |
| 加密目标 | 只加密真正需要的值(API Key、Secret、令牌等),其余保持普通列 |
| 日志 | 切勿在日志中输出明文/令牌/密钥(尤其是 DEBUG 日志) |
| 备份 | 备份数据时,若无密钥则数据无意义 |
| 测试 | 单元测试验证 encrypt -> decrypt 一致,迁移(原始明文)处理正确 |
9. 结语
- 低级 AES 实现难懂且易出错。
- 采用 Fernet 可以:
- 组合安全算法
- 自动处理随机 IV、时间戳、完整性校验
- 提供简洁 API
在 Django 中:
- 使用
EncryptedTextField+_secret/secret模式, - 开发者只需处理明文,数据库始终存储加密值。
结论
- 敏感数据 = 必须加密后存储
- 复杂加密算法交给成熟库 只要遵守这两点,Django 服务的安全水平就能大幅提升。 🔐

目前沒有評論。