Django: Auth: Signup

20th August 2020 at 2:19pm
Django: Auth

Django 的注册流程相对复杂。下面选择在 admin 界面中新建用户的流程,来梳理下这其中的逻辑。

如果你用的是默认的 Django 模板,那么 Django 的 auth 模块是默认启用的;如果你没有用,参考 官方文档 打开 auth,并 打开 admin 功能

执行过初始化时的 python manage.py migrate 后,会在数据库建立 auth_user 表用来存放用户信息,对应 django.contrib.auth.models.User。将 Django server 运行起来,打开新建用户的页面,写入用户名密码:

这个页面是由 django.contrib.auth.admin.UserAdmin 通过 Django admin 提供的定制能力创建的。页面内容主要定义在:

@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    add_form_template = 'admin/auth/user/add_form.html'
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('username', 'password1', 'password2'),
        }),
    )
    add_form = UserCreationForm
    # ...
  • add_form_template 定义了页面模板
  • add_fieldsets 定义了什么字段需要填写
  • add_form 定义了提交的数据由哪个 Form 来验证及处理

提交数据后,处理逻辑来到了 django.contrib.auth.forms.UserCreationForm。这段逻辑比较重要:

from django.contrib.auth.models import User

class UserCreationForm(forms.ModelForm):
    """
    A form that creates a user, with no privileges, from the given username and
    password.
    """
    error_messages = {
        'password_mismatch': _('The two password fields didn’t match.'),
    }
    password1 = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        help_text=password_validation.password_validators_help_text_html(),
    )
    password2 = forms.CharField(
        label=_("Password confirmation"),
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        strip=False,
        help_text=_("Enter the same password as before, for verification."),
    )

    class Meta:
        model = User
        fields = ("username",)
        field_classes = {'username': UsernameField}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self._meta.model.USERNAME_FIELD in self.fields:
            self.fields[self._meta.model.USERNAME_FIELD].widget.attrs['autofocus'] = True

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError(
                self.error_messages['password_mismatch'],
                code='password_mismatch',
            )
        return password2

    def _post_clean(self):
        super()._post_clean()
        # Validate the password after self.instance is updated with form data
        # by super().
        password = self.cleaned_data.get('password2')
        if password:
            try:
                password_validation.validate_password(password, self.instance)
            except forms.ValidationError as error:
                self.add_error('password2', error)

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user

Django form 对数据做校验的流程看 官方文档。这段代码的重点是:

  1. clean_password2() 中校验了用户两次输入的密码是否一致
  2. _post_clean() 是个内部 hook,会在做完全部校验后执行。这里对密码做了额外校验(比如长度不能太短、不能跟用户名太相似、不能是常见密码)。放在 _post_clean() 是因为 Django 在这里已经 prepopulate 了(即生成好了还未保存进数据库)一个 User 实例 self.instance,它包含了用户名等信息,可以给 password validator 做用户名相似性的校验
  3. 最后 save() 将用户数据保存到数据库。它先用了 commit=False 来生成一个 django.contrib.auth.models.User 的实例,再调用 set_password() 将密码字段从明文(self.cleaned_data["password1"])转成密文再存入数据库(下面详谈)

如果你不用这个 UserCreationForm要实现自己的注册逻辑,那么你可以参考它实现类似的字段校验,并且在存入数据库时也一样调用 set_password()

set_password() 的行为描述在 官方文档,具体如下:

使用默认配置时,Django 会采用 PBKDF2 算法及 SHA256 摘要算法,迭代 150000 次(Django 2.2)来生成密码:

  1. 生成一个密码学安全的随机字符串作为盐
  2. 使用 PBKDF2 算法,将密码原文、盐及迭代次数(默认 150000 次)作为参数,并以 SHA256 为摘要算法,生成一串 hash 值
  3. "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) 形式将密码保存至数据库(默认 auth_user 表)

代码入口在 django.contrib.auth.base_user.AbstractBaseUser.set_password

代码比较冗长,不再列出。

See Also