70. drf练习之多方式登录
1. 多方式登录需求
用户名+密码
手机号+密码
邮箱+密码

auth的user表有用户名和邮箱字段,没有手机号字段,需要扩写
2. 扩写auth的user表,增加手机号字段
2.1 概念
关键点:
如果要扩写auth的user表,必须在迁移之前扩写
迁移之后再扩写就会出问题
迁移之后再扩写:
1. 删库(删除之前保存数据)
2. 删除迁移文件
app的
django内置app的admin和auth
2.2 代码实现
[1] 删除文件
删除库

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

删除django内置app的admin和auth的迁移文件
在总路由中点击admin进入到admin的init文件中

在admin的init中打开文件

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

删除admin和auth的迁移文件


[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字段

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')),
]
创建一个超级用户

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

错误用户名密码登录

正确用户名密码登录

邮箱密码登录

手机号密码登录

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字段时
使用正确的用户名密码登录会报错用户名已存在

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 重写字段
重写字段之后正确用户名登录时不再报用户名已存在的错误

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迁移至数据库中,添加基础数据

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})
用户名密码错误登录失败

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

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')),
]
未登录访问查询所有接口

登录

带着登录的token查询所有

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

浙公网安备 33010602011771号