DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

时间:2023-03-08 18:43:09

用户登录

自定义用户登录字段处理

用户的登录时通过 手机号也可以进行登录

需要重写登录验证逻辑

from django.contrib.auth.backends import ModelBackend

class CustomBackend(ModelBackend):

    def authenticate(self, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(mobile=username))
# 前端的用户传递过来的密码和数据库的保存密码是不一致的, 因此需要使用 check_password 的方式进行比对
if user.check_password(password):
return user
except Exception as e:
return None

登录逻辑

通过 login 接口进入验证, 调用默认重写后的验证逻辑进行处理

 url(r'^login/', obtain_jwt_token)

验证成功后会返回 token

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

用户注册

用户注册基于 手机号注册

验证码发送基于 云片网 提供的技术支持

验证码逻辑

验证码API 接口

# 配置手机验证码发送 的 url
router.register(r'codes', SmsCodeViewset, base_name="codes")

验证码序列化组件

选取序列化方式的时候以为不是全部的字段都需要用上, 因此不需用到 ModelSerializer

需要对前端拿到的  mobile 字段进行相关的验证

是否注册, 是否合法, 以及频率限制

# 手机验证序列化组件
# 不使用 ModelSerializer, 并不需要所有的字段, 会有麻烦
class SmsSerializer(serializers.Serializer):
mobile = serializers.CharField(max_length=11) # 验证手机号码
# validate_ + 字段名 的格式命名
def validate_mobile(self, mobile): # 手机是否注册
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用户已经存在") # 验证手机号码是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手机号码非法") # 验证码发送频率
# 当前时间减去一分钟( 倒退一分钟 ), 然后发送时间要大于这个时间, 表示还在一分钟内
one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
raise serializers.ValidationError("距离上一次发送未超过60s")
return mobile

验证码视图

视图主要处理 验证码生成发送相关逻辑

具体的云片网接口对接处理详情官网查阅

# 发送短信验证码
class SmsCodeViewset(CreateModelMixin, viewsets.GenericViewSet):
serializer_class = SmsSerializer # 生成四位数字的验证码
def generate_code(self): seeds = ""
random_str = []
for i in range(4):
random_str.append(choice(seeds))
return "".join(random_str) # 重写 create 方法
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 验证后即可取出数据
mobile = serializer.validated_data["mobile"]
yun_pian = YunPian(APIKEY)
code = self.generate_code()
sms_status = yun_pian.send_sms(code=code, mobile=mobile) if sms_status["code"] != 0:
return Response({
"mobile": sms_status["msg"]
}, status=status.HTTP_400_BAD_REQUEST)
else:
# 确认无误后需要保存数据库中
code_record = VerifyCode(code=code, mobile=mobile)
code_record.save()
return Response({
"mobile": mobile
}, status=status.HTTP_201_CREATED)

云片验证码工具文件

# _*_ coding:utf-8 _*_
from YtShop.settings import APIKEY __author__ = "yangtuo"
__date__ = "2019/4/15 20:25"
import requests
import json # 云片网短信发送功能类
class YunPian(object): def __init__(self, api_key):
self.api_key = api_key
self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json" def send_sms(self, code, mobile):
parmas = {
"apikey": self.api_key,
"mobile": mobile,
"text": "您的验证码是{code}。如非本人操作,请忽略本短信".format(code=code)
} response = requests.post(self.single_send_url, data=parmas)
re_dict = json.loads(response.text)
return re_dict if __name__ == "__main__":
yun_pian = YunPian(APIKEY)
yun_pian.send_sms("", "") # 参数为 code 以及 mobile

配置文件

需要用到两个配置添加

# 手机号码的验证正则式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$" # 云片网的 APIKEY 设置
APIKEY = "2480f562xxxxxxxxxxxxxcb7673f8"

注册逻辑

注册 API 接口

# 配置用户注册的 url
router.register(r'users', UserViewset, base_name="users")

注册序列化组件

用户注册需要的字段较多

每个字段都有些独有的特殊裁定

用户名  要进行重复判断

验证码  要进行有效期, 正确性判断

密码  设置 输入框为密码格式

在最后回传的时候 code 是不需要的, 因此可以删除掉

# 用户注册
class UserRegSerializer(serializers.ModelSerializer):
"""
max_length 最大长度
min_length 最小长度
label 显示名字
help_text 帮助提示信息
error_messages 错误类型映射提示
blank 空字段提示
required 必填字段提示
max_length 超长度提示
min_length 过短提示
write_only 只读, 序列化的时候忽略字段, 不再返回给前端页面, 用于去除关键信息(密码等)或者某些不必要字段(验证码)
style 更改输入标签显示类型
validators 可以指明一些默认的约束类
UniqueValidator 约束唯一
UniqueTogetherValidator 联合约束唯一
UniqueForMonthValidator
UniqueForDateValidator
UniqueForYearValidator
....
"""
code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, label="验证码",
error_messages={
"blank": "请输入验证码",
"required": "请输入验证码",
"max_length": "验证码格式错误",
"min_length": "验证码格式错误"
},
help_text="验证码") # validators 可以指明一些默认的约束类, 此处的 UniqueValidator 表示唯一约束限制不能重名
username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")]) # style 可以设置为密文状态
password = serializers.CharField(
style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True,
) # 用户表中的 password 是需要加密后再保存的, 次数需要重写一次 create 方法
# 当然也可以不这样做, 这里的操作利用 django 的信号来处理, 详情见 signals.py
# def create(self, validated_data):
# user = super(UserRegSerializer, self).create(validated_data=validated_data)
# user.set_password(validated_data["password"])
# user.save()
# return user # 对验证码的验证处理
# validate_ + 字段对个别字段进行单一处理
def validate_code(self, code): # 如果使用 get 方式需要处理两个异常, 分别是查找到多个信息的情况以及查询到0信息的情况的异常
# 但是使用 filter 方式查到多个就以列表方式返回, 如果查询不到数据就会返回空值, 各方面都很方便
# try:
# verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
# except VerifyCode.DoesNotExist as e:
# pass
# except VerifyCode.MultipleObjectsReturned as e:
# pass # 前端传过来的所有的数据都在, initial_data 字典里面, 如果是验证通过的数据则保存在 validated_data 字典中
verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
if verify_records:
last_record = verify_records[0] # 时间倒叙排序后的的第一条就是最新的一条
# 当前时间回退5分钟
five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
# 最后一条短信记录的发出时间小于5分钟前, 表示是5分钟前发送的, 表示过期
if five_mintes_ago > last_record.add_time:
raise serializers.ValidationError("验证码过期")
# 根据记录的 验证码 比对判断
if last_record.code != code:
raise serializers.ValidationError("验证码错误")
# return code # 没必要保存验证码记录, 仅仅是用作验证
else:
raise serializers.ValidationError("验证码错误") # 对所有的字段进行限制
def validate(self, attrs):
attrs["mobile"] = attrs["username"] # 重命名一下
del attrs["code"] # 删除无用字段
return attrs class Meta:
model = User
fields = ("username", "code", "mobile", "password")

注册视图

class UserViewset(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
serializer_class = UserRegSerializer
queryset = User.objects.all() # 重写 create 函数来完成注册后自动登录功能
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer) re_dict = serializer.data
payload = jwt_payload_handler(user)
# token 的添加只能用此方法, 此方法通过源码阅读查找到位置为
re_dict["token"] = jwt_encode_handler(payload)
# 自定义一个字段加入进去
re_dict["name"] = user.name if user.name else user.username headers = self.get_success_headers(serializer.data)
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers) def get_object(self):
return self.request.user def perform_create(self, serializer):
return serializer.save()

信号量处理工具文件

注册后的信息回传给数据库保存的时候 密码是按照是未加密状态保存

此处需要进行加密后才可以, 因此这里可以用信号量来处理, post_save 触发

在此触发流程中完成加密后保存数据库

# _*_ coding:utf-8 _*_
__author__ = "yangtuo"
__date__ = "2019/4/15 20:25" from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
from django.contrib.auth import get_user_model User = get_user_model() @receiver(post_save, sender=User) # post_save 信号类型, sender 能触发信号的模型
def create_user(sender, instance=None, created=False, **kwargs): # created 是否新建( update 就不会被识别 )
# instance 表示保存对象, 在这里是被保存的 user 对象
if created:
password = instance.password
instance.set_password(password)
instance.save()
# Token.objects.create(user=instance)
# user 对象的保存一般是要伴随着 token 的, 这里已经使用 JWT 方式了, 因此就不需要这种 token 了.

注册后自动登录逻辑

目标预期

用户注册后自动跳转到主页

同时要实现注册用户已登录状态

需求分析

用户注册相关的操作本质是从前端拿到数据传送到后端通过 相关的 view 进行操作

本质是 底层的 create 方法, 默认的方法只能实现用户创建无法实现其他附加

( DRF 的视图 功能嵌套 层次详情点击 这里查看  )

因此我们需要重写 create 方法

定位重写 create 方法

可见只有序列化类的更新和推送, 无其他功能

默认的 create 方法

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

如果想实现自动登录, 首先本质就是加入用户登录的状态, 即 token 的生成和保存

本次项目使用的是 JWT 作为 token 方案, 因此 需要考究在 JWT 的源码中 token 如何生成

定位 token 生成源码查阅

JWT 的源码入口 ( URL 对接视图 )

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

往上找到视图类

这里是做了一层很简单的封装, 以及可以看到熟悉的 as_view()

不过我们目前不关心这个, 这里同样基于 DRF 视图中类似

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

视图类中找到序列化处理

这个 serializer_class 就是对应着序列化类的处理

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

序列化处理中对 token 的处理

其实我们已经知道了JWT 的方式是不会基于数据库的, 因此他们的序列化类中的是没有任何的字段

通过各种方法来实现字段的计算和生成

以下是全部的 相关逻辑

class JSONWebTokenSerializer(Serializer):
"""
Serializer class used to validate a username and password. 'username' is identified by the custom UserModel.USERNAME_FIELD. Returns a JSON Web Token that can be used to authenticate later calls.
"""
def __init__(self, *args, **kwargs):
"""
Dynamically add the USERNAME_FIELD to self.fields.
"""
super(JSONWebTokenSerializer, self).__init__(*args, **kwargs) self.fields[self.username_field] = serializers.CharField()
self.fields['password'] = PasswordField(write_only=True) @property
def username_field(self):
return get_username_field() def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
} if all(credentials.values()):
user = authenticate(**credentials) if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg) payload = jwt_payload_handler(user) return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)

定位到 token 的生成代码

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

可见 需要使用到 jwt_payload_handler 方法以及 jwt_encode_handler 方法

因此生成 token 就是在这里了, 为了生成 token 我们需要用到这两个方法, 使用方法就完全模仿源码即可

完成 create 重写

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class UserViewset(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
serializer_class = UserRegSerializer
queryset = User.objects.all() # 重写 create 函数来完成注册后自动登录功能
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
   
  # 此处为自定义的 token 的生成 
re_dict = serializer.data
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
# 顺便把 用户名一并传过去
re_dict["name"] = user.name if user.name else user.username headers = self.get_success_headers(serializer.data)
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers) def get_object(self):
return self.request.user def perform_create(self, serializer):
return serializer.save()

用户退出

不需要再写一个 logout 接口

JWT 不需要服务器这边进行相关的操作

只需要前端进行一个 cookie 的清空然后跳转即可

跳转到 登录页面或者主页皆可

    loginOut(){
cookie.delCookie('token');
cookie.delCookie('name');
//重新触发store
//更新store数据
this.$store.dispatch('setInfo');
//跳转到登录
this.$router.push({name: 'login'})
},

用户个人中心

retrieve 方式添加

用户中心的数据来源是对单一用户的详细数据请求, 因此需要在原有基础上加上对  retrieve 的处理

 mixins.RetrieveModelMixin

用户 id 传递

同时因为对单一用户的请求需要指明用户id, 有两种方式可以传递

第一种 直接在数据里面提供当前用户 id

第二种 重写 get_object 获取当前用户

# 因为要涉及到 个人中心的操作需要传递过去 用户的 id, 重写 get_object 来实现
def get_object(self):
return self.request.user

权限分离

用户中心必须指定当前用户只能访问自己, 因此需要对是否登录进行验证

但是当前视图的其他类型请求比如 create 的注册则不需要进行验证, 因此  permission_classes 无法满足需求

源码剖析

在继承了  ViewSetMixin 之后内部的 initialize_request 方面里面的 提供了 .action 在 request 中可以对请求类型进行分离

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

同时 APIView 内部的  get_permissions  方法负责提取认证类型, 因此重写此方法即可完成

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

此为 源码, 可见是直接使用一个列表表达式来获取当前视图的 permission_classes 里面的所有认证方式

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

实现重写

基于我们自己的需求进行重写, 利用 action 进行分流

注意其他未设置的最后一定要返回空

    # permission_classes = (permissions.IsAuthenticated, )  # 因为根据类型的不同权限的认证也不同, 不能再统一设置了
def get_permissions(self):
if self.action == "retrieve":
return [permissions.IsAuthenticated()]
elif self.action == "create":
return []
return []

序列化组件分离

创建组件

之前设置的序列化组件是为了注册用的, 只采集了注册相关的字段, 无法满足用户中心的其他字段处理

因此需要重新设置一个用户详情的 序列化组件

# 用户详情信息序列化类
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("name", "gender", "birthday", "email", "mobile")

源码剖析

同样是基于对 action 的方法进行分流, 对于 action 的位置在 权限分流的部分有图,

在   GenericAPIView 中存在 get_serializer_class  方法, 用于获取当前视图中的 序列化组件

DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理

实现重写

基于 action 进行分流, 然后进行对 get_serializer_class 进行重写

实现方式类似于 权限的分流

   def get_serializer_class(self):
if self.action == "retrieve":
return UserDetailSerializer
elif self.action == "create":
return UserRegSerializer
return UserDetailSerializer

完整代码

用户视图代码

# 用户视图
class UserViewset(mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
serializer_class = UserRegSerializer
queryset = User.objects.all() authentication_classes = (JSONWebTokenAuthentication, authentication.SessionAuthentication) # 用户中心的个人详情数据不能再基于统一设置的 UserRegSerializer 了
# 用户注册和 用户详情分为了两个序列化组件
# self.action 必须要继承了 ViewSetMixin 才有此功能
# get_serializer_class 的源码位置在 GenericAPIView 中
def get_serializer_class(self):
if self.action == "retrieve":
return UserDetailSerializer
elif self.action == "create":
return UserRegSerializer
return UserDetailSerializer # permission_classes = (permissions.IsAuthenticated, ) # 因为根据类型的不同权限的认证也不同, 不能再统一设置了
# get_permissions 的源码在 APIview 中
def get_permissions(self):
if self.action == "retrieve":
return [permissions.IsAuthenticated()]
elif self.action == "create":
return []
return [] # 重写 create 函数来完成注册后自动登录功能
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer) """
此处重写的源码分析以及 相关的逻辑
详情点击此博客
https://www.cnblogs.com/shijieli/p/10726194.html
"""
re_dict = serializer.data
payload = jwt_payload_handler(user)
# token 的添加只能用此方法, 此方法通过源码阅读查找到位置为
re_dict["token"] = jwt_encode_handler(payload)
# 自定义一个字段加入进去
re_dict["name"] = user.name if user.name else user.username headers = self.get_success_headers(serializer.data)
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers) # 因为要涉及到 个人中心的操作需要传递过去 用户的 id, 重写 get_object 来实现
def get_object(self):
return self.request.user def perform_create(self, serializer):
return serializer.save()

用户相关序列化组件

# _*_ coding:utf-8 _*_
__author__ = "yangtuo"
__date__ = "2019/4/15 20:25" import re
from rest_framework import serializers
from django.contrib.auth import get_user_model
from datetime import datetime
from datetime import timedelta
from rest_framework.validators import UniqueValidator from .models import VerifyCode
from YtShop.settings import REGEX_MOBILE User = get_user_model() # 手机验证序列化组件
# 不使用 ModelSerializer, 并不需要所有的字段, 会有麻烦
class SmsSerializer(serializers.Serializer):
mobile = serializers.CharField(max_length=11) # 验证手机号码
# validate_ + 字段名 的格式命名
def validate_mobile(self, mobile): # 手机是否注册
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用户已经存在") # 验证手机号码是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手机号码非法") # 验证码发送频率
# 当前时间减去一分钟( 倒退一分钟 ), 然后发送时间要大于这个时间, 表示还在一分钟内
one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
raise serializers.ValidationError("距离上一次发送未超过60s")
return mobile # 用户详情信息序列化类
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("name", "gender", "birthday", "email", "mobile") # 用户注册
class UserRegSerializer(serializers.ModelSerializer):
"""
max_length 最大长度
min_length 最小长度
label 显示名字
help_text 帮助提示信息
error_messages 错误类型映射提示
blank 空字段提示
required 必填字段提示
max_length 超长度提示
min_length 过短提示
write_only 只读, 序列化的时候忽略字段, 不再返回给前端页面, 用于去除关键信息(密码等)或者某些不必要字段(验证码)
style 更改输入标签显示类型
validators 可以指明一些默认的约束类
UniqueValidator 约束唯一
UniqueTogetherValidator 联合约束唯一
UniqueForMonthValidator
UniqueForDateValidator
UniqueForYearValidator
....
"""
code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, label="验证码",
error_messages={
"blank": "请输入验证码",
"required": "请输入验证码",
"max_length": "验证码格式错误",
"min_length": "验证码格式错误"
},
help_text="验证码") # validators 可以指明一些默认的约束类, 此处的 UniqueValidator 表示唯一约束限制不能重名
username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")]) # style 可以设置为密文状态
password = serializers.CharField(
style={'input_type': 'password'}, help_text="密码", label="密码", write_only=True,
) # 用户表中的 password 是需要加密后再保存的, 次数需要重写一次 create 方法
# 当然也可以不这样做, 这里的操作利用 django 的信号来处理, 详情见 signals.py
# def create(self, validated_data):
# user = super(UserRegSerializer, self).create(validated_data=validated_data)
# user.set_password(validated_data["password"])
# user.save()
# return user # 对验证码的验证处理
# validate_ + 字段对个别字段进行单一处理
def validate_code(self, code): # 如果使用 get 方式需要处理两个异常, 分别是查找到多个信息的情况以及查询到0信息的情况的异常
# 但是使用 filter 方式查到多个就以列表方式返回, 如果查询不到数据就会返回空值, 各方面都很方便
# try:
# verify_records = VerifyCode.objects.get(mobile=self.initial_data["username"], code=code)
# except VerifyCode.DoesNotExist as e:
# pass
# except VerifyCode.MultipleObjectsReturned as e:
# pass # 前端传过来的所有的数据都在, initial_data 字典里面 ,
verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
if verify_records:
last_record = verify_records[0] # 时间倒叙排序后的的第一条就是最新的一条
# 当前时间回退5分钟
five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
# 最后一条短信记录的发出时间小于5分钟前, 表示是5分钟前发送的, 表示过期
if five_mintes_ago > last_record.add_time:
raise serializers.ValidationError("验证码过期")
# 根据记录的 验证码 比对判断
if last_record.code != code:
raise serializers.ValidationError("验证码错误")
# return code # 没必要保存验证码记录, 仅仅是用作验证
else:
raise serializers.ValidationError("验证码错误") # 对所有的字段进行限制
def validate(self, attrs):
attrs["mobile"] = attrs["username"] # 重命名一下
del attrs["code"] # 删除无用字段
return attrs class Meta:
model = User
fields = ("username", "code", "mobile", "password")