news 2026/9/24 13:51:14

django-allauth 内置 MFA:从 django-allauth-2fa 迁移 TOTP 密钥与恢复码的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
django-allauth 内置 MFA:从 django-allauth-2fa 迁移 TOTP 密钥与恢复码的完整指南
  • 后端
  • 认证鉴权
  • 身份认证

【免费下载链接】django-allauth

Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication. 🔁 Mirror of https://codeberg.org/allauth/django-allauth/

项目地址:https://gitcode.com/gh_mirrors/dj/django-allauth
点击查看免费下载

本文是面向正在使用第三方扩展django-allauth-2fa、希望切换到 django-allauth 内置多因素认证(MFA)实现的团队的技术迁移指南。文章以官方参考迁移代码为核心,结合本仓库中allauth.mfa的模型、适配器与内部流程源码,逐段讲解 TOTP 密钥与恢复码的迁移原理、数据格式差异、加密存储钩子以及迁移后的验证方式,帮助读者在不停用既有用户双因素保护的前提下完成平滑切换。

迁移背景:为什么需要迁移 TOTP 密钥与恢复码

django-allauth 从某个版本起将多因素认证(MFA)作为内置能力提供,涵盖:

  • 基于 TOTP 的动态口令认证;
  • 基于恢复码(recovery codes)的备援认证;
  • 恢复码的查看、下载与重新生成;
  • WebAuthn 凭据与 Passkey 登录(默认关闭)。

内置实现位于 allauth/mfa 包中。而历史项目中很多团队使用独立的第三方扩展 django-allauth-2fa 实现双因素认证,该扩展基于django-otpTOTPDeviceStaticDevice模型保存密钥。两套实现的存储模型完全不同,因此在切换时必须将既有用户的TOTP 密钥恢复码一并迁入 django-allauth 的Authenticator模型,否则已开启双因素的用户将无法登录。

本仓库官方文档 docs/mfa/django-allauth-2fa.rst 为此提供了参考迁移代码,下文将逐段剖析。

迁移前置条件:启用内置 MFA 应用

在编写迁移命令之前,需要先让内置 MFA 应用处于可用状态。

安装 mfa 扩展依赖

内置 MFA 依赖qrcode等额外包,需要安装mfaextras:

pip install "django-allauth[mfa]"

注册应用并执行数据迁移

在项目的settings.py中将allauth.mfa加入INSTALLED_APPS(参见 docs/mfa/introduction.rst):

INSTALLED_APPS = [ ... 'allauth', 'allauth.account', 'allauth.mfa', ... ]

随后执行数据迁移,创建Authenticator模型对应的表:

python manage.py migrate

Authenticator的表结构由 allauth/mfa/migrations/0001_initial.py 及其后续迁移定义,其中 0003_authenticator_type_uniq.py 为每个用户施加了(user, type)上的条件唯一约束:对totprecovery_codes两种类型,同一用户只能各有一条记录。这一点对迁移脚本的编写有直接影响(见下文)。

新旧数据模型对照

迁移的核心是理解两套模型的数据格式差异。

django-otp 侧的模型(迁移来源)

  • django_otp.plugins.otp_totp.models.TOTPDevice:保存 TOTP 设备,关键字段为key(十六进制字符串)、confirmed(是否已确认启用)与user_id
  • django_otp.plugins.otp_static.models.StaticDevice:保存静态令牌(恢复码)设备,其下通过token_set关联一组StaticToken,即恢复码本体。

allauth.mfa 侧的模型(迁移目标)

目标模型为 allauth/mfa/models.py 中的Authenticator

class Authenticator(models.Model): class Type(models.TextChoices): RECOVERY_CODES = "recovery_codes", _("Recovery codes") TOTP = "totp", _("TOTP Authenticator") WEBAUTHN = "webauthn", _("WebAuthn") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) type = models.CharField(max_length=20, choices=Type.choices) data = models.JSONField() created_at = models.DateTimeField(default=timezone.now) last_used_at = models.DateTimeField(null=True)

关键差异:

  • 所有认证方式统一存放在一张表,通过type区分,totprecovery_codeswebauthn三种类型的业务数据全部放在dataJSON 字段中;
  • TOTP 的data结构为{"secret": <加密后的 Base32 密钥>},密钥必须为 Base32 编码(与标准 TOTP otpauth URI 的格式一致);
  • 恢复码的data结构为{"migrated_codes": [<加密后的恢复码>, ...]},这是一个迁移专用字段:正常新建的恢复码记录使用seed+used_mask的派生方案,而migrated_codes专门用于承接从外部系统迁入的、无法用新方案重新派生的旧恢复码。

为什么 TOTP 密钥要转成 Base32

django-otp 的TOTPDevice.key是随机字节的十六进制表示,而 django-allauth 的 TOTP 实现(见 allauth/mfa/totp/internal/auth.py)在generate_totp_secret()中使用base64.b32encode(random_bytes)生成 Base32 密钥,并在hotp_value()中以base64.b32decode(secret.encode("ascii"), casefold=True)解码。因此迁移时要把十六进制密钥解码回原始字节、再重新编码为 Base32:

secret = base64.b32encode(bytes.fromhex(totp.key)).decode("ascii")

只有完成这一转换,用户手机上的 Authenticator 应用才能继续基于同一密钥生成正确的动态口令。

参考迁移命令:完整代码

官方文档给出的迁移脚本是一个 Django management command(BaseCommand),全文如下(来自 docs/mfa/django-allauth-2fa.rst):

import base64 from allauth.mfa.adapter import get_adapter from allauth.mfa.models import Authenticator from django.core.management.base import BaseCommand from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_totp.models import TOTPDevice class Command(BaseCommand): def handle(self, **options): adapter = get_adapter() authenticators = [] for totp in TOTPDevice.objects.filter(confirmed=True).iterator(): recovery_codes = set() for sdevice in StaticDevice.objects.filter(confirmed=True, user_id=totp.user_id).iterator(): recovery_codes.update(sdevice.token_set.values_list("token", flat=True)) secret = base64.b32encode(bytes.fromhex(totp.key)).decode("ascii") totp_authenticator = Authenticator( user_id=totp.user_id, type=Authenticator.Type.TOTP, data={"secret": adapter.encrypt(secret)}, ) authenticators.append(totp_authenticator) authenticators.append( Authenticator( user_id=totp.user_id, type=Authenticator.Type.RECOVERY_CODES, data={ "migrated_codes": [adapter.encrypt(c) for c in recovery_codes], }, ) ) Authenticator.objects.bulk_create(authenticators)

逐段剖析迁移脚本

1. 获取适配器实例

adapter = get_adapter()

get_adapter()定义于 allauth/mfa/adapter.py,它依据MFA_ADAPTER设置(默认"allauth.mfa.adapter.DefaultMFAAdapter")实例化适配器:

def get_adapter() -> DefaultMFAAdapter: return import_attribute(app_settings.ADAPTER)()

迁移脚本通过适配器调用encrypt(),从而自动继承项目自定义的加密策略——这一点非常关键,详见下文"密钥加密存储"小节。

2. 遍历已确认的 TOTP 设备

for totp in TOTPDevice.objects.filter(confirmed=True).iterator():

confirmed=True过滤出用户已完成验证、实际可用的 TOTP 设备;未确认的设备(如用户扫码后从未输入过验证码)不具备认证价值,不参与迁移。.iterator()避免一次性将所有设备载入内存,适合大用户量场景。

3. 收集同一用户的恢复码

recovery_codes = set() for sdevice in StaticDevice.objects.filter(confirmed=True, user_id=totp.user_id).iterator(): recovery_codes.update(sdevice.token_set.values_list("token", flat=True))

对每个已确认的 TOTP 设备,查找同一用户的已确认StaticDevice,将其token_set中的全部静态令牌收进一个set。使用set有两个作用:

  • 自动去重(同一恢复码在多个设备中重复出现时只保留一份);
  • 为后续列表推导提供确定性的迭代行为。

注意:恢复码是绑定到 TOTP 用户的,因此以user_id=totp.user_id为关联键聚合,而不是为每个 StaticDevice 单独建一条Authenticator记录。

4. 构造 TOTP Authenticator 记录

secret = base64.b32encode(bytes.fromhex(totp.key)).decode("ascii") totp_authenticator = Authenticator( user_id=totp.user_id, type=Authenticator.Type.TOTP, data={"secret": adapter.encrypt(secret)}, )
  • bytes.fromhex(totp.key):把 django-otp 的十六进制密钥还原为原始字节;
  • base64.b32encode(...).decode("ascii"):重新编码为 Base32 字符串,与 allauth/mfa/totp/internal/auth.py 中generate_totp_secret()的输出格式对齐;
  • data={"secret": adapter.encrypt(secret)}:密钥经适配器加密后存入 JSON 字段。TOTP 校验时,allauth/mfa/totp/internal/auth.py 的TOTP.validate_code()会先decrypt(self.instance.data["secret"])再执行 HMAC-SHA1 动态口令比对,与迁移写入的格式一一对应。

5. 构造恢复码 Authenticator 记录

authenticators.append( Authenticator( user_id=totp.user_id, type=Authenticator.Type.RECOVERY_CODES, data={ "migrated_codes": [adapter.encrypt(c) for c in recovery_codes], }, ) )

每个恢复码在存入前同样经过adapter.encrypt(),整份列表挂在migrated_codes键下。这条记录与 TOTP 记录一起被收集,随后统一bulk_create

6. 批量写入

Authenticator.objects.bulk_create(authenticators)

一次性批量创建所有用户的认证记录,性能优于逐条save()

迁移后恢复码的消费逻辑:migrated_codes 的生命周期

迁入的恢复码并不是静态数据,它会参与后续的认证与校验。其消费逻辑定义在 allauth/mfa/recovery_codes/internal/auth.py 的RecoveryCodes类中:

def _get_migrated_codes(self) -> list[str] | None: codes = self.instance.data.get("migrated_codes") if codes is not None: return [decrypt(code) for code in codes] return None def _validate_migrated_code(self, code: str) -> bool | None: migrated_codes = self._get_migrated_codes() if migrated_codes is None: return None try: idx = migrated_codes.index(code) except ValueError: return False else: migrated_codes = self.instance.data["migrated_codes"] migrated_codes.pop(idx) self.instance.data["migrated_codes"] = migrated_codes self.instance.save() return True def validate_code(self, code: str) -> bool: ret = self._validate_migrated_code(code) if ret is not None: return ret ...

从中可以看出三个重要事实:

  1. 优先消费迁移代码validate_code()首先尝试_validate_migrated_code(),只有migrated_codes字段不存在(返回None)时才走基于seed的新方案;
  2. 一次性使用:匹配到某个迁移恢复码后,会将其从migrated_codes列表中pop并持久化,该恢复码随即失效,不可重复使用——这符合恢复码的安全惯例;
  3. 与原生恢复码共存语义一致:迁移代码同样遵守"用完即废"的语义,只是底层存储从seed+used_mask位图换成了显式列表。

仓库中的测试 tests/apps/mfa/recovery_codes/test_auth.py 对此进行了验证:

def test_migrated_codes(db, user): auth = Authenticator(user=user, data={"migrated_codes": ["abc", "def"]}) ... assert rc.instance.data["migrated_codes"] == []

该测试断言:两个迁移代码依次使用后,migrated_codes列表被清空,从侧面印证了"使用即移除"的实现细节。

密钥加密存储:adapter.encrypt / decrypt 钩子

迁移脚本全程通过adapter.encrypt()写入密钥与恢复码,这是 django-allauth 为密钥存储安全预留的扩展点。默认实现位于 allauth/mfa/adapter.py:

def encrypt(self, text: str) -> str: """Secrets such as the TOTP key are stored in the database. This hook can be used to encrypt those so that they are not stored in the clear in the database. """ return text def decrypt(self, encrypted_text: str) -> str: """Counter part of ``encrypt()``.""" text = encrypted_text return text
  • 默认行为是原样返回,即密钥明文存入dataJSON 字段;
  • 生产环境建议通过设置MFA_ADAPTER指向自定义适配器,覆盖encrypt/decrypt实现真正的加密(如基于项目的SECRET_KEY派生密钥的对称加密),避免 TOTP 密钥与恢复码以明文形式落库;
  • 所有读写密钥的路径(TOTP 校验、恢复码校验)都经由allauth.mfa.utilsencrypt/decrypt薄封装(见 allauth/mfa/utils.py),因此只要适配器实现了对称加解密,迁移写入与运行时读取即自动匹配。

运行迁移前的核对清单

将上述脚本落地为项目内的 management command(例如放入yourapp/management/commands/migrate_allauth_2fa.py)后,建议按以下清单核对后再执行:

  1. 依赖顺序:脚本依赖django_otp的模型,应确认项目在迁移完成前仍安装着 django-allauth-2fa / django-otp;
  2. 备份数据库:迁移脚本会创建新记录,执行前对mfa_authenticator及旧otp_totp_totpdeviceotp_static_staticdevice相关表做备份,以便回滚核对;
  3. 幂等性:参考脚本未做幂等保护,重复运行会因 0003_authenticator_type_uniq.py 引入的(user, type)条件唯一约束而违反约束报错(同一用户出现两条totp或两条recovery_codes记录)。如需可重复执行,应先对目标用户过滤"已存在Authenticator则跳过";
  4. 空恢复码处理:若某用户只有 TOTP 设备而没有 StaticDevice,recovery_codes集合为空,此时仍会创建一条migrated_codes=[]的恢复码记录。可以接受(等价于无恢复码),也可以在脚本中跳过空集合;
  5. 迁移后验证:迁移完成后,用一位测试用户的 TOTP 密钥生成动态口令、用一条旧恢复码各做一次登录验证,确认两条路径(TOTP.validate_codeRecoveryCodes.validate_code)均正常;
  6. 收尾卸载:确认全部用户迁移成功且验证通过后,方可移除django-allauth-2fadjango-otp相关依赖与INSTALLED_APPS条目。

迁移后的运行时配置(可选)

迁移完成后,内置 MFA 的默认行为即可满足基本需求。若需调整,可参考 docs/mfa/configuration.rst 中与迁移直接相关的设置项:

  • MFA_ADAPTER(默认"allauth.mfa.adapter.DefaultMFAAdapter"):自定义适配器路径,用于实现密钥加密等行为定制;
  • MFA_RECOVERY_CODE_COUNT(默认10)、MFA_RECOVERY_CODE_DIGITS(默认8):仅影响新生成的恢复码;迁移进来的旧恢复码保持原样,不受这两个参数影响;
  • MFA_TOTP_PERIOD(默认30)、MFA_TOTP_DIGITS(默认6)、MFA_TOTP_TOLERANCE(默认0):TOTP 的步长、位数与时钟容差。迁移的密钥与这些参数解耦——只要手机端应用使用同一 Base32 密钥,就能在相同时间窗口内计算出正确口令;
  • MFA_RECOVERY_CODES_SHOW_ONCE(默认False):是否只在生成时展示一次恢复码,对已迁入的恢复码同样生效。

小结

从 django-allauth-2fa 切换到 django-allauth 内置 MFA,本质是一次数据迁移:把 django-otp 的TOTPDevice.key(十六进制)转为 Base32 密钥写入Authenticatortotp记录,把StaticDevice的令牌写入recovery_codes记录的migrated_codes字段,全程经由适配器的encrypt()钩子保证与运行时读取格式一致。参考官方迁移脚本(docs/mfa/django-allauth-2fa.rst)配合本仓库 allauth/mfa 的源码理解,即可在保证既有用户双因素保护不中断的前提下完成平滑切换。

  • 后端
  • 认证鉴权
  • 身份认证

【免费下载链接】django-allauth

Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication. 🔁 Mirror of https://codeberg.org/allauth/django-allauth/

项目地址:https://gitcode.com/gh_mirrors/dj/django-allauth
点击查看免费下载
上一篇:【亲测免费】 MuPDF 项目常见问题解决方案
下一篇:【亲测免费】 Whisper ASR Webservice 项目推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 13:49:49

【Coze】【图像】节日海报生成工作流

今天给大家演示一个「节日海报自动生成」的 Coze 工作流,通过节日关键词输入,即可一键生成节日氛围浓厚、视觉风格统一的 AI 海报图像。整个流程由多步大模型处理、图像生成模块、画板排版组件等组成,自动提取节日代表元素,生成精美提示词并驱动图像合成,同时还能定制化祝…

作者头像 李华
网站建设 2026/9/24 13:46:37

Flask 扩展 OpenID 认证

在构建 Web 应用时,用户身份认证是不可或缺的部分。传统的用户系统需要开发者自行设计注册、登录、会话管理等功能,既费时又容易引发安全隐患。而 OpenID 提供了一种更为便捷和安全的认证机制,通过第三方身份服务(如 Google、Yahoo 等)实现统一登录,极大提升了开发效率和…

作者头像 李华
网站建设 2026/9/24 13:46:31

Flask 扩展 Mail 邮件

Flask 是一个轻量级的 Web 框架,本身并不自带邮件功能。Flask-Mail 是一个官方推荐的扩展,用于在 Flask 应用中轻松集成邮件发送功能。它封装了对邮件协议的底层操作,提供了直观的接口用于构建、发送邮件以及处理相关的配置逻辑。这个扩展可以和常见的邮件服务商(如 Gmail、…

作者头像 李华