在 Django 模型中安全存储密钥(Fernet 版本)
安全是开发者的责任 – 直接把原文存进数据库是不可取的。
1. 为什么需要加密?
- API Key / Secret / OAuth token 等一旦泄露, 就相当于整个服务被攻破。
- 把它们硬编码在
settings.py, → Git 仓库、部署产物、开发者笔记本等都可能直接暴露。 - 即使放在环境变量里, 但如果直接存进数据库, 数据库被攻破时就会以明文泄露。
另一个需要注意的点:
- 密码(password)
- 服务器不需要知道原文 → 哈希(BCrypt、Argon2)
- API Key / Secret / token
- 服务器需要再次使用原文 → 可解密加密
总结
- 需要再次使用的敏感数据 → 加密后存储
- 不需要再次使用的数据(如密码) → 哈希后存储
2. Fernet 是什么?
cryptography.fernet.Fernet 是一个 高级 API,一次性提供了以下功能:
- 内部使用安全算法(AES + HMAC 等)
- 自动处理 随机 IV、时间戳、完整性校验(HMAC)
- 开发者只需 妥善保管密钥,
encrypt(plain_text)→ 生成 token 字符串decrypt(token)→ 还原明文
示例代码:
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. 设计要点
- 数据库只存 加密后的 token 字符串
- 模型/视图/服务层始终只看到 解密后的明文
- 采用
_secret+secret的命名模式, _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 token(str)
- 读取时:Fernet token(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 token)
@property
def secret(self):
"""外部使用时始终返回明文"""
return self._secret
@secret.setter
def secret(self, value: str):
"""赋值时自动加密"""
self._secret = value
_secret:真正的数据库列名,存的是加密字符串。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)
return Response({"secret": obj.secret})
在实际项目中,常见做法是:
- 用
obj.secret调用外部 API - 绝不将明文直接暴露给前端
7. 使用 Fernet 时需要注意的点
7-1. 搜索/过滤限制
Fernet 的加密结果每次都会不同:
obj1.secret = "abc"
obj2.secret = "abc"
虽然明文相同,但 _secret 存储的 token 完全不同。
因此,以下查询无效:
MyModel.objects.filter(_secret="abc") # 无意义
MyModel.objects.filter(_secret=obj._secret) # 只能按 token 比较
如果需要按明文搜索/排序,建议:
- 添加 搜索用列(哈希或前缀)
- 或重新评估是否需要加密
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、Token)加密,其他保持普通列 |
| 日志 | 切勿在日志中输出明文、token 或密钥(尤其是 DEBUG 日志) |
| 备份 | 备份数据若无密钥即无意义,确保密钥安全 |
| 测试 | 单元测试验证 encrypt -> decrypt 一致,迁移时处理明文数据 |
9. 结语
实现低级 AES 需要大量细节,容易出错。使用 Fernet 可以:
- 采用安全的算法组合
- 自动处理随机 IV、时间戳、完整性校验
- 提供简洁的 API
在 Django 中,只需自定义 EncryptedTextField 并使用 _secret / secret 模式,开发者即可写出只处理明文的代码,而数据库始终存储加密值。
结论
- 敏感数据 = 必须加密后存储
- 复杂加密逻辑交给成熟库 只要遵循这两条,Django 服务的安全性会显著提升。 🔐

目前没有评论。