用户的登陆时经过 手机号也能够进行登陆html
须要重写登陆验证逻辑前端
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 接口进入验证, 调用默认重写后的验证逻辑进行处理python
url(r'^login/', obtain_jwt_token)
验证成功后会返回 token数据库
用户注册基于 手机号注册django
验证码发送基于 云片网 提供的技术支持json
# 配置手机验证码发送 的 url router.register(r'codes', SmsCodeViewset, base_name="codes")
选取序列化方式的时候觉得不是所有的字段都须要用上, 所以不需用到 ModelSerializer后端
须要对前端拿到的 mobile 字段进行相关的验证api
是否注册, 是否合法, 以及频率限制服务器
# 手机验证序列化组件 # 不使用 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
视图主要处理 验证码生成发送相关逻辑cookie
具体的云片网接口对接处理详情官网查阅
# 发送短信验证码 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"
# 配置用户注册的 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 方法
可见只有序列化类的更新和推送, 无其余功能
若是想实现自动登陆, 首先本质就是加入用户登陆的状态, 即 token 的生成和保存
本次项目使用的是 JWT 做为 token 方案, 所以 须要考究在 JWT 的源码中 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 咱们须要用到这两个方法, 使用方法就彻底模仿源码便可
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 的处理
mixins.RetrieveModelMixin
同时由于对单一用户的请求须要指明用户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")