用户登录
自定义用户登录字段处理
用户的登录时通过 手机号也可以进行登录
需要重写登录验证逻辑
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
用户注册
用户注册基于 手机号注册
验证码发送基于 云片网 提供的技术支持
验证码逻辑
验证码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 = "1234567890" 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("2019", "") # 参数为 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 方法
可见只有序列化类的更新和推送, 无其他功能
如果想实现自动登录, 首先本质就是加入用户登录的状态, 即 token 的生成和保存
本次项目使用的是 JWT 作为 token 方案, 因此 需要考究在 JWT 的源码中 token 如何生成
定位 token 生成源码查阅
JWT 的源码入口 ( URL 对接视图 )
往上找到视图类
这里是做了一层很简单的封装, 以及可以看到熟悉的 as_view()
不过我们目前不关心这个, 这里同样基于 DRF 视图中类似
视图类中找到序列化处理
这个 serializer_class 就是对应着序列化类的处理
序列化处理中对 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 的生成代码
可见 需要使用到 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 中可以对请求类型进行分离
同时 APIView 内部的 get_permissions 方法负责提取认证类型, 因此重写此方法即可完成
此为 源码, 可见是直接使用一个列表表达式来获取当前视图的 permission_classes 里面的所有认证方式
实现重写
基于我们自己的需求进行重写, 利用 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 方法, 用于获取当前视图中的 序列化组件
实现重写
基于 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")