70. drf练习之多方式登录

1. 多方式登录需求

用户名+密码

手机号+密码

邮箱+密码

image

auth的user表有用户名和邮箱字段,没有手机号字段,需要扩写

2. 扩写auth的user表,增加手机号字段

2.1 概念

关键点:

  如果要扩写auth的user表,必须在迁移之前扩写

  迁移之后再扩写就会出问题

 

迁移之后再扩写:

  1. 删库(删除之前保存数据)

  2. 删除迁移文件

    app的

    django内置app的admin和auth

2.2 代码实现

[1] 删除文件

删除库

image

删除迁移文件

此时migrations目录下没有迁移文件,只有为空的init文件,这个文件不能删除

image

删除django内置app的admin和auth的迁移文件

在总路由中点击admin进入到admin的init文件中

image

在admin的init中打开文件

image

通过admin的init文件可以看到admin与auth目录,contrib里面是djagngo自带的app

image

删除admin和auth的迁移文件

image

image

[2] 定义模型表重新迁移

app01的models.py

# 扩写:自定义auth_user表
from django.contrib.auth.models import AbstractUser

class UserInfo(AbstractUser):
    # 只需要写自定义字段,默认字段在基类中
    mobile = models.CharField(max_length=11)

项目的settings.py

使用自定义的表替换auth_user表

AUTH_USER_MODEL = 'app01.UserInfo'

重新迁移之前需要新建数据库,可以使用原库的名称

重新迁移:makemigrations  migrate

auth_user表替换为userinfo表,默认基础上新增mobile字段

image

3. 前端如何传

{username:'用户名/159666999/123@mit.com',password:'123456'}

key为username不变,而不是username+pwd、mobile+pwd、email+pwd

由于key值恒定,不能使用自动签发,需要自定义登录接口。因为自动签发只能识别用户名+密码,不能识别手机号和邮箱

4. 多方式登录接口简易版

models.py与settings.py的AUTH_USER_MODEL与上面保持一致

app01的views.py

from rest_framework.viewsets import ViewSet
import re
from .models import UserInfo
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.response import Response

class UserView(ViewSet):
    def login(self, request):
        '''
        1.取出用户名和密码
        2.通过正则匹配
            如果是手机号,就查询  手机号+密码
            如果是邮箱,就查询  邮箱+密码
            如果是其它类型,就查询  用户名+密码
        3.校验成功签发token:access、refresh
        4.返回给前端{code, msg, access, refresh}
        '''
        username = request.data.get('username')
        password = request.data.get('password')
        if re.match(r'^1[3-9][0-9]{9}$', username):
            # 根据手机号获取user对象
            # 前端传过来的password是明文,不能直接filter(mobile=username, password=password)
            user = UserInfo.objects.filter(mobile=username).first()  # 前端传来的手机号、邮箱、用户名统一用username接收
        elif re.match(r'^.+@.+$', username):  # .表示任意字符开头,中间@,以任意字符结尾
            user = UserInfo.objects.filter(email=username).first()  # 前端传来的手机号、邮箱、用户名统一用username接收
        else:
            user = UserInfo.objects.filter(username=username).first()  # 不是手机号和邮箱就按用户名查询
        if user and user.check_password(password):  # 用户名密码校验成功签发token
            # 根据get_token函数查看access、refresh的获取方法
            refresh_obj = RefreshToken.for_user(user)
            refresh = str(
                refresh_obj)  # 由于refresh_obj是RefreshToken对象:一个工具箱,里面自带一个工具(access_token),因此上面变量名不能和这个变量名一致,否则字符串会覆盖
            access = str(refresh_obj.access_token)
            return Response({'code': 100, 'msg': '登录成功', 'access': access, 'refresh': refresh})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误'})

app01的urls.py

from django.urls import path
from .views import UserView

urlpatterns = [
    # /api/v1/app01/login11/
    path('login11/', UserView.as_view({'post': 'login'})),
]

项目的urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/app01/', include('app01.urls')),
]

创建一个超级用户

image

数据库中添加手机号,必须为11位

image

错误用户名密码登录

image

正确用户名密码登录

image

邮箱密码登录

image

手机号密码登录

image

 

5. 多方式登录接口进阶版

5.1 代码

校验逻辑放在序列化组件中

app01的serializer.py

from rest_framework import serializers
from .models import UserInfo
import re
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.exceptions import ValidationError

class LoginSerializer(serializers.ModelSerializer):  # 序列化类的作用:序列化、反序列化、数据校验;这里只用来做校验
    # 重写字段,不设置校验规则
    # username = serializers.CharField()
    class Meta:
        model = UserInfo
        fields = ['username', 'password']  # 只需要这两个字段用来校验,不需要所有字段__all__

    def validate(self, attrs):
        # 完成校验和签发token,并且把access和refresh放到self.context中
        username = attrs.get('username')  # attrs 只包含校验通过的字段
        password = attrs.get('password')
        if re.match(r'^1[3-9][0-9]{9}$', username):
            # 根据手机号获取user对象
            # 前端传过来的password是明文,不能直接filter(mobile=username, password=password)
            user = UserInfo.objects.filter(mobile=username).first()  # 前端传来的手机号、邮箱、用户名统一用username接收
        elif re.match(r'^.+@.+$', username):  # .表示任意字符开头,中间@,以任意字符结尾
            user = UserInfo.objects.filter(email=username).first()  # 前端传来的手机号、邮箱、用户名统一用username接收
        else:
            user = UserInfo.objects.filter(username=username).first()  # 不是手机号和邮箱就按用户名查询
        if user and user.check_password(password):  # 用户名密码校验成功签发token
            # 根据get_token函数查看access、refresh的获取方法
            refresh_obj = RefreshToken.for_user(user)
            refresh = str(
                refresh_obj)  # 由于refresh_obj是RefreshToken对象:一个工具箱,里面自带一个工具(access_token),因此上面变量名不能和这个变量名一致,否则字符串会覆盖
            access = str(refresh_obj.access_token)
            # 将token放到上下文字典context
            self.context['refresh'] = refresh
            self.context['access'] = access
        else:
            # 校验失败,抛异常
            raise ValidationError('用户名或密码错误')
        return attrs

app01的views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from .serializer import LoginSerializer

class UserView(GenericViewSet):  # 要用序列化类
    serializer_class = LoginSerializer

    def login(self, request):
        '''
        1.取出用户名和密码
        2.通过正则匹配
            如果是手机号,就查询  手机号+密码
            如果是邮箱,就查询  邮箱+密码
            如果是其它类型,就查询  用户名+密码
        3.校验成功签发token:access、refresh
        4.返回给前端{code, msg, access, refresh}
        # '''
        serializer = self.get_serializer(data=request.data)
        # 对序列化器进行数据校验,校验失败时直接抛出验证异常,而不是返回 False。
        # 只要运行这句话,就会运行序列化器中字段自己的规则:局部钩子、全局钩子
        serializer.is_valid(raise_exception=True)
        # 运行完用户名密码校验步骤,同时签发了token
     # 在序列化组件中通过self.context放入token,此时的self即为序列化类,
      这里通过变量名serializer获取到了序列化类,因此调用方式为serializer.context
access = serializer.context.get('access') # 上下文字典包含view、request对象,序列化组件里手动添加token到context refresh = serializer.context.get('refresh') return Response({'code': 100, 'msg': '登录成功', 'access': access, 'refresh': refresh})

当以上序列化组件中不重写username字段时

使用正确的用户名密码登录会报错用户名已存在

image

5.2 报错分析

[1] 前提

1. 校验运行顺序:字段内置校验器 → 局部钩子 validate_xxx → 全局钩子 validate。
2. 当前报错发生在字段校验阶段,代码不会进入全局钩子 validate。

前提:模型表继承了AbstractUser,源码如下

username = models.CharField(
        _("username"),
        max_length=150,
        unique=True,
        help_text=_(
            "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
        ),
        validators=[username_validator],
        error_messages={
            "unique": _("A user with that username already exists."),
        },
    )

[2] 完整运行步骤

请求:POST /api/v1/app01/login11/
请求体:{"username":"testuser","password":"123456"}

步骤 1:路由分发

匹配 path ('login11/', UserView.as_view ({'post': 'login'})),运行UserView.login 方法。

步骤 2:视图实例化序列化器

serializer = self.get_serializer(data=request.data)

get_serializer 获取 LoginSerializer,传入 request.data。
LoginSerializer 为 ModelSerializer,关联 UserInfo,读取 Meta.fields 指定的 username、password。
读取 UserInfo 继承自 AbstractUser 的 username 字段定义,自动为 username 挂载 UniqueValidator (queryset=UserInfo.objects.all ())。

步骤 3:运行serializer.is_valid (raise_exception=True)

is_valid 内部运行反序列化校验流程:

1. 取出前端传入 data,对每个字段做类型转换、基础字段校验。
2. 运行字段上挂载的所有校验器(包含 UniqueValidator)
  UniqueValidator 的逻辑:去 UserInfo 表查询,判断数据库中是否已经存在该 username 的记录。
  登录场景:用户名 testuser 在数据库确实存在。校验器判定不满足 unique,直接抛出校验错误:

{
    "username": [
        "A user with that username already exists."
    ]
}

3. 校验抛出异常,raise_exception=True,终止后续所有校验逻辑。
后续全部代码不再执行:
  不会执行局部钩子、不会进入自定义的全局钩子 validate,不会做手机号 / 邮箱正则判断、不会查询用户比对密码、不会生成 JWT token。

步骤 4:异常直接返回前端

is_valid 抛出 ValidationError,DRF 捕获异常,把错误信息直接返回给前端,视图后续代码不再运行。

核心原因

ModelSerializer 设计目标为模型新增、修改操作。新增场景下 unique 校验用于防止重复用户名。

登录业务需要传入数据库已存在的用户名,ModelSerializer 自动挂载的 UniqueValidator 与登录业务冲突。

UniqueValidator 属于序列化器内存校验,执行时机早于数据库写入操作,本次报错由序列化器校验器抛出,并非数据库约束报错。

概念区分

1. 模型字段 unique=True:提供数据库唯一约束;ModelSerializer 会基于该属性自动生成 UniqueValidator 校验器。
2. UniqueValidator:序列化器层面校验,执行在代码阶段,早于数据库操作,本次报错来源于该校验器。

步骤精简版

前端 POST (username,password)
路由匹配 UserView.login
实例 LoginSerializer (data=request.data),ModelSerializer 读取 UserInfo 模型,username 挂载 UniqueValidator
serializer.is_valid ()
  基础字段校验
  UniqueValidator 查询数据库,username 已存在 → 抛出 ValidationError
  校验终止,不会进入全局钩子 validate
DRF 返回错误信息 {"username":["A user with that username already exists."]}
视图后续代码不运行

解决方案

序列化组件中重写字段
username = serializers.CharField()

5.3 重写字段

重写字段之后正确用户名登录时不再报用户名已存在的错误

image

5.4 补充:context

为什么要 self.context ['access']、serializer.context.get ('access') 的方式,而不直接 self.access、serializer.get ('access')

核心结论

self.context是 DRF 序列化器内置预留的上下文容器,专门用来在「序列化器 ↔ 视图」之间传递额外临时数据;
不能直接用 self.access / serializer.get(access),二者底层对象、生命周期、作用域完全不一样。

1. 先看 self.context ['access'] 为什么可行

context是序列化器实例自带的字典属性,在视图运行get_serializer(data=..., context=xxx)时自动注入,默认自带 view、request。

  在序列化器钩子 validate 内:self 代表当前序列化器实例,self.context 就是这个内置字典。
  字典可以动态增加键值:self.context['access'] = access,把 token 存入这个共享字典。
  字典属于实例属性,跨函数可以读取:视图里拿到同一个序列化器对象,通过serializer.context.get('access')读取写入的值。

  本质:利用 DRF 原生提供的共享字典做跨层数据传递。

2. 为什么不能写 self.access

self.access 是给序列化器实例新增一个普通实例属性,语法上不会报错,但不推荐,不是 DRF 约定写法。

# 语法能跑,但不规范
self.access = access
# 视图读取
serializer.access

问题:

1. 语义混淆:序列化器实例属性一般用来存放字段、数据、校验状态,业务返回 token 不属于序列化器本身的属性,属于校验完成后的附属结果。
2. 可读性差:其他开发者看到 self.access 无法第一时间分清是模型字段、序列化器内置属性,还是自定义临时变量;context一看就知道是跨组件传递的附加信息。
3. 项目习惯:DRF 生态中,校验钩子向外传递非模型字段的额外返回数据,约定放在 context。

3. 为什么不能 serializer.get ('access')

serializer是序列化器实例对象,对象没有.get()方法。

  .get()是字典的方法。serializer.data才是字典;serializer本身不是字典。
  serializer.get('access')会直接抛出异常 AttributeError: 'LoginSerializer' object has no attribute 'get'`。

如果想从序列化器的数据字典拿值,只能写 serializer.data.get('xxx')。
但 serializer.data的来源是模型映射 /attrs 校验字段,只能存放反序列化输出的模型相关字段,不能手动往 data 里面塞 access、refresh。

禁止往 serializer.data 写入 token 的原因:
 serializer.data 由校验后的 attrs生成,在 is_valid阶段固化;强行修改 data 会破坏 DRF 序列化内部状态,还会触发额外序列化逻辑,容易产生隐藏 bug。token 不属于模型字段,不应该放到 data 里。

4. 方案对比

写法类型是否推荐说明
self.context['access'] + serializer.context.get('access') 字典传递,DRF 原生约定 ✅推荐 专门用来传递校验产生的附加业务数据,不污染序列化 data
self.access + serializer.access 实例属性挂载 ❌不推荐 语法可运行,但不符合 DRF 设计思想,语义不清晰
serializer.get('access') 调用对象.get () ❌报错 序列化器实例不是字典,无 get 方法
serializer.data['access'] 写入序列化输出 data ❌不推荐 污染序列化数据,容易引发校验、渲染异常

5. 总结

1. context 是 DRF 内置共享字典,专门用来在视图和序列化器之间传递附加信息,是框架约定;
2. self.access 是自定义实例变量,语法可行但不符合 DRF 语义规范;
3. serializer.get('access') 直接报错,序列化器对象不是字典,没有 get 方法;
4. token 不属于模型字段,不适合写入 serializer.data。

6. 多方式登录之使用系统自带login

不用自定义登录函数,使用系统自带

from rest_framework_simplejwt.views import token_obtain_pair

1.配置路由  path('login/', token_obtain_pair)

2.只需要写序列化类,在序列化类的validate函数中实现校验和签发token

3.项目的settings.py中使用自定义序列化类     

SIMPLE_JWT={
"TOKEN_OBTAIN_SERIALIZER":"app01.serializer.SelfDefineTokenObtainPairSerializer",
}

7. 总结

如果用AUTH的user表作为用户表,无论是否扩展该表

登录函数:

  可以不写,只需要写序列化类配置文件中配置即可

认证是固定的,无需写认证类:

class BookView(APIView):
    authentication_classes = [JWTTokenUserAuthentication]
    permission_classes = [IsAuthenticated]

8. 自定义用户表

8.1 签发

以上自定义用户表继承了基类AbstractUser,现在需要纯自定义用户表

app01的models.py

class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)  # 密码没有加密
    age = models.IntegerField()
    hobby = models.IntegerField(choices=((1, 'sing'), (2, 'rap')), default=2)

makemigrations-migrate迁移至数据库中,添加基础数据

image

app01的serializer.py

from .models import User

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    def validate(self, attrs):
        username = attrs.get('username')
        password = attrs.get("password")
        user = User.objects.filter(username=username, password=password).first()
        if user:
            refresh_obj = RefreshToken.for_user(user)
            refresh = str(
                refresh_obj)  # 由于refresh_obj是RefreshToken对象:一个工具箱,里面自带一个工具(access_token),因此上面变量名不能和这个变量名一致,否则字符串会覆盖
            access = str(refresh_obj.access_token)
            # 将token放到上下文字典context
            self.context['refresh'] = refresh
            self.context['access'] = access
        else:
            raise ValidationError("用户名或密码错误")
        return attrs

自定义登录函数app01的views.py与之前保持不变

from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from .serializer import LoginSerializer

class UserView(GenericViewSet):  # 要用序列化类
    serializer_class = LoginSerializer

    def login(self, request):
        '''
        1.取出用户名和密码
        2.通过正则匹配
            如果是手机号,就查询  手机号+密码
            如果是邮箱,就查询  邮箱+密码
            如果是其它类型,就查询  用户名+密码
        3.校验成功签发token:access、refresh
        4.返回给前端{code, msg, access, refresh}
        # '''
        serializer = self.get_serializer(data=request.data)
        # 对序列化器进行数据校验,校验失败时直接抛出验证异常,而不是返回 False。
        # 只要运行这句话,就会运行序列化器中字段自己的规则:局部钩子、全局钩子
        serializer.is_valid(raise_exception=True)
        # 运行完用户名密码校验步骤,同时签发了token
        access = serializer.context.get('access')  # 上下文字典包含view、request对象,序列化组件里手动添加token到context
        refresh = serializer.context.get('refresh')
        return Response({'code': 100, 'msg': '登录成功', 'access': access, 'refresh': refresh})

用户名密码错误登录失败

image

用户名密码正确登录签发成功

image

8.2 自定义认证类

app01的models.py

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)  # 密码没有加密
    age = models.IntegerField()
    hobby = models.IntegerField(choices=((1, 'sing'), (2, 'rap')), default=2)

app01的serializer.py

from rest_framework import serializers
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.exceptions import ValidationError
from .models import User

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    def validate(self, attrs):
        username = attrs.get('username')
        password = attrs.get("password")
        user = User.objects.filter(username=username, password=password).first()
        if user:
            refresh_obj = RefreshToken.for_user(user)
            refresh = str(
                refresh_obj)  # 由于refresh_obj是RefreshToken对象:一个工具箱,里面自带一个工具(access_token),因此上面变量名不能和这个变量名一致,否则字符串会覆盖
            access = str(refresh_obj.access_token)
            # 将token放到上下文字典context
            self.context['refresh'] = refresh
            self.context['access'] = access
        else:
            raise ValidationError("用户名或密码错误")
        return attrs

app01的auth.py

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
from rest_framework_simplejwt.tokens import AccessToken as AuthToken
from .models import User

class LoginAuth(BaseAuthentication):
    def authenticate(self, request):
        # 取出前端传入的token,根据请求头: HTTP_Authorization(格式为Bearer ...)
        token = request.META.get('HTTP_AUTHORIZATION')
        if token and token.startswith('Bearer '):
            token = token.split(' ')[-1]  # 变量名token是原始字符串(前端传过来切割后的字符串)
            # 校验token仿照源码JWTTokenUserAuthentication-JWTStatelessUserAuthentication-JWTAuthentication-authenticate函数-get_validated_token函数
            try:
                # 查看AuthToken的位置,get_validated_token函数中api_settings.AUTH_TOKEN_CLASSES,在外部settings文件中AUTH_TOKEN_CLASSES位于r_f_simplejwt.tokens.AccessToken
                validate_token = AuthToken(token)  # 是对象不是字符串,是AuthToken实例对象(simplejwt的AccessToken对象,已经验签解析完成)
                # 获取登录用户方式一:
                user = User.objects.filter(pk=validate_token.get('user_id')).first()
                # 获取登录用户方式二:通过以下get_user函数
                # user = self.get_user(validate_token)
                return user, validate_token  # 返回元组(user, auth),第二个元素会赋值给request.auth
            except TokenError as e:
                raise InvalidToken(str(e))  # str(e)是当前版本的标准写法
        else:
            raise AuthenticationFailed('没有token或token格式不合规')

    # 获取登录用户方式二:仿造JWTAuthentication重写函数
    # def get_user(self, validate_token):
    #     # validate_token如何获取登录用户:base64解码--取出user_id--去User表中查询返回
    #     user_id = validate_token.get('user_id')
    #     user = User.objects.filter(pk=user_id).first()
    #     return user

app01的views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from .auth import LoginAuth
from rest_framework.viewsets import GenericViewSet
from .serializer import LoginSerializer

class BookView(APIView):
    authentication_classes = [LoginAuth]

    def get(self, request):
        return Response('查询所有')

class UserView(GenericViewSet):  # 要用序列化类
    serializer_class = LoginSerializer

    def login(self, request):
        '''
        1.取出用户名和密码
        2.通过正则匹配
            如果是手机号,就查询  手机号+密码
            如果是邮箱,就查询  邮箱+密码
            如果是其它类型,就查询  用户名+密码
        3.校验成功签发token:access、refresh
        4.返回给前端{code, msg, access, refresh}
        # '''
        serializer = self.get_serializer(data=request.data)
        # 对序列化器进行数据校验,校验失败时直接抛出验证异常,而不是返回 False。
        # 只要运行这句话,就会运行序列化器中字段自己的规则:局部钩子、全局钩子
        serializer.is_valid(raise_exception=True)
        # 运行完用户名密码校验步骤,同时签发了token
        access = serializer.context.get('access')  # 上下文字典包含view、request对象,序列化组件里手动添加token到context
        refresh = serializer.context.get('refresh')
        return Response({'code': 100, 'msg': '登录成功', 'access': access, 'refresh': refresh})

app01的urls.py

from django.urls import path
from .views import UserView, BookView

urlpatterns = [
    path('books/', BookView.as_view()),
    # /api/v1/app01/login11/
    path('login11/', UserView.as_view({'post': 'login'})),
]

项目的urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/app01/', include('app01.urls')),
]

 未登录访问查询所有接口

image

登录

image

带着登录的token查询所有

image

8.3 自定义认证类补充

继承JWTTokenUserAuthentication,只需重写get_user函数,返回自定义用户表的用户即可

不需要重写authenticate函数

from rest_framework_simplejwt.authentication import JWTTokenUserAuthentication

class Login(JWTTokenUserAuthentication):
    def get_user(self, validated_token):
        user = User.objects.filter(pk=validated_token.get('user_id')).first()
        return user

 

posted @ 2026-10-02 18:15  pythondjango  阅读(4)  评论(0)    收藏  举报