DRF: Auth: Implement: Signup

20th August 2020 at 2:19pm
DRF: Auth: Implement

如何在 DRF 中实现注册新用户功能。

首先应该理解 Django 原有的 流程。DRF 与 Django 的注册流程不同的是:

  • Django 有现成的 UserCreationForm,校验表单中提交的用户名密码信息并实际保存到数据库;DRF 没有,需要自己实现
  • Django 的 <form> 中需要带有 csrf token,但 DRF 中不需要带;如果用户在未登陆状态,对它实施 CSRF 攻击意义也不大

下面的实现上重度参考了 Django: Auth: Signup 中 Django 本身的实现,以及这个 StackOverflow 帖子。实现的重点写在注释中。

Serializer 实现

# app/serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model, password_validation

# get_user_model 可以获得 custom user model
UserModel = get_user_model()

class UserSerializer(serializers.ModelSerializer):
    # password1 及 password2 表示用户在注册表单中,填写的「密码」和「确认密码」两个位置
    # write_only 因为它不需要出现在读接口中
    # input_type 使它在 DRF 的 browsable API 页面中是一个 <input type="password"> 而不是 <input type="text">
    # min_length 及 max_length 是额外的要求,因为 Django 默认只限制密码长度为 128 位,看这里:
    #   django.contrib.auth.base_user.AbstractBaseUser
    password1 = serializers.CharField(
        label="Password", write_only=True, style={'input_type': 'password'},
        min_length=8, max_length=20
    )
    password2 = serializers.CharField(
        label="Password confirmation", write_only=True, style={'input_type': 'password'},
        min_length=8, max_length=20
    )

    def validate_password1(self, value):
        # 生成一个 UserModel 对象传进去,password validation 才可以做密码跟用户名的相似度检查
        password_validation.validate_password(
            value, UserModel(username=self.initial_data['username'], email=self.initial_data['email'])
        )
        return value

    def validate(self, data):
        # 多 field 组合校验,在这里做
        if data['password1'] != data['password2']:
            raise serializers.ValidationError("The two password fields didn't match")
        return data

    def create(self, validated_data):
        user = UserModel.objects.create(
            username=validated_data['username'],
            email=validated_data['email'],
        )
        # set_password 会对明文密码做 hash
        user.set_password(validated_data['password1'])
        user.save()

        return user

    class Meta:
        model = UserModel
        fields = ("id", "username", "email", "password1", "password2")

        extra_kwargs = {
            # Django 默认的 django.contrib.auth.base_user.AbstractBase 对用户名长度限制宽松,这里加强
            'username': {
                'max_length': 16, 'min_length': 6,
                'help_text': 'Required. 6 to 16 characters required. Letters, digits and @/./+/-/_ only.'
            },

            # Django 默认非必填,这里要求必填
            'email': {'required': True},
        }

Views 实现

# app/views.py
from django.contrib.auth import get_user_model
from rest_framework import permissions
from rest_framework.generics import CreateAPIView

from app.serializers import UserSerializer


class SignupView(CreateAPIView):
    model = get_user_model()
    permission_classes = [
            # 使用 AllowAny 使匿名用户也可以注册。
                # DRF 默认的权限会使得匿名用户无法调用 POST 方法。
        permissions.AllowAny
    ]
    serializer_class = UserSerializer

Urls 配置

# project/urls.py
from app.views import SignupView

urlpatterns = [
    # ...
    path('signup/', SignupView.as_view(), name='signup'),
]

这个实现还有一些不足之处,比如 Django 默认的 user 实现中,email 是可以重复的。应该写一个 custom user 将其转为不可重复。