在 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)         # 解密

只要理解了这段代码,就能轻松使用。

我们需要关注的两件事

  1. 密钥管理(环境变量、Secret Manager 等)
  2. 在 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. 密钥轮换策略

不建议长期使用同一密钥。可采用以下思路:

  1. 初始阶段只用 DJANGO_FERNET_KEY
  2. 后期引入 OLD_KEYNEW_KEY: * 加密:仅用 NEW_KEY * 解密:尝试 OLD_KEYNEW_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 服务的安全性会显著提升。 🔐

image