如何以加密邮箱作为用户名实现Django用户认证?
解决方案:Django加密邮箱作为用户名的高效认证
核心问题是避免全库解密匹配,同时满足GDPR的加密要求,以下是可行的方案:
1. 存储邮箱的加盐哈希+加密值(推荐)
Fernet的随机IV特性导致相同邮箱加密结果不同,但我们可以通过加盐哈希快速定位可能的用户,再解密验证:
- 用户创建时:
- 对原始邮箱生成加盐哈希(用Django内置的
make_password,自带加盐和哈希算法,安全可靠),存在单独字段(如email_hash) - 用Fernet加密原始邮箱,存储为
username字段
- 对原始邮箱生成加盐哈希(用Django内置的
- 用户登录时:
- 对输入的原始邮箱计算相同的加盐哈希
- 用哈希值查询数据库,筛选出匹配的用户(哈希冲突概率极低,通常只有1条)
- 解密该用户的
username字段,对比是否与输入邮箱一致,同时验证密码
代码示例
自定义用户模型(或扩展现有模型)
from django.contrib.auth.models import AbstractUser from django.db import models from django.contrib.auth.hashers import make_password import cryptography.fernet as fernet from django.conf import settings class CustomUser(AbstractUser): email_hash = models.CharField(max_length=128, unique=True) def save(self, *args, **kwargs): # 仅在创建用户或更新邮箱时生成哈希和加密用户名 if not self.pk or hasattr(self, '_original_username') and self.username != self._original_username: # 加密邮箱作为用户名 fernet_obj = fernet.Fernet(settings.FERNET_KEY) self.username = fernet_obj.encrypt(self.email.encode()).decode() # 生成邮箱的加盐哈希 self.email_hash = make_password(self.email) # 保存原始用户名用于对比 self._original_username = self.username super().save(*args, **kwargs)
自定义认证后端
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.hashers import check_password, make_password from .models import CustomUser import cryptography.fernet as fernet from django.conf import settings class EncryptedEmailAuthBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): # 这里的username是用户输入的原始邮箱 if not username or not password: return None # 计算输入邮箱的加盐哈希,查询匹配用户 email_hash = make_password(username) users = CustomUser.objects.filter(email_hash=email_hash) fernet_obj = fernet.Fernet(settings.FERNET_KEY) for user in users: try: # 解密存储的用户名(加密邮箱) decrypted_email = fernet_obj.decrypt(user.username.encode()).decode() if decrypted_email == username and check_password(password, user.password): return user except fernet.InvalidToken: # 无效加密令牌,跳过该用户 continue return None
配置认证后端
在settings.py中替换默认后端:
AUTHENTICATION_BACKENDS = [ 'yourapp.backends.EncryptedEmailAuthBackend', # 保留默认后端可选,根据需求调整 # 'django.contrib.auth.backends.ModelBackend', ] AUTH_USER_MODEL = 'yourapp.CustomUser'
2. 关于Fernet加密的可行性
Fernet每次加密结果不同是因为使用了随机IV,但解密后会得到原始字符串,所以完全可以实现无错误认证,核心是解决高效查询匹配的问题,上面的哈希方案是最优选择。
不推荐的方案:确定性加密
如果使用固定IV的加密方式(放弃Fernet的随机IV),相同邮箱会得到相同的加密结果,这样可以直接用加密后的邮箱查询,但这会带来安全风险:攻击者可以通过加密值识别重复邮箱,违反GDPR的隐私要求,因此不建议使用。
内容的提问来源于stack exchange,提问作者Kevin Sullivan
相关产品推荐
相关产品推荐

