【Django REST framework电商项目笔记】第07章 手机注册和用户登录(下)

vue和jwt接口调试

login.vue 中查看登录的具体逻辑

login({
          username:this.userName, //当前页码
          password:this.parseWord
      }).then((response)=> {
            console.log(response);
            //本地存储用户信息
            cookie.setCookie('name',this.userName,7);
            cookie.setCookie('token',response.data.token,7)
            //存储在store
            // 更新store数据
            that.$store.dispatch('setInfo');
            //跳转到首页页面
            this.$router.push({ name: 'index'})
          })

获取到当前的用户名和密码 这个用户名和密码来自当前的data()中
本地存储设置了cookie的名字和值,token和值。并设置了7天过期

我们的jwt 调用的是django自带的auth与userProfile中数据进行对比。而我们如果使用手机注册,就会导致验证失败。因为默认是用用户名和密码去查的。

自定义django用户认证函数

1、首先在setting中设置变量:

# 设置邮箱和用户名和手机号均可登录
AUTHENTICATION_BACKENDS = (
    'users.views.CustomBackend',

)

2、在 user/view 中添加 CustomBackend 类

class CustomBackend(ModelBackend):
    """
    自定义用户验证规则
    """

    def authenticate(self, username=None, password=None, **kwargs):
        try:
            # 不希望用户存在两个, get只能有一个
            user = User.objects.get(Q(username=username)|Q(mobile=username))

            # django的后台中密码加密的,所以不能password==password
            # UserProfile继承的AbstractUser中有
            # def check_password(self, raw_password)
            if user.check_password(password):
                return user
        except Exception as e:
            return None

JWT的过期时间设置

# jwt相关的设置
import datetime
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

云片网发送短信验证码

注册会用到一些高级的 Serializer
写一个接口:发送短信
1、云片网
一个账号有多个子账号,每一个子账号都会有一个 api key
发送短信验证码前会验证这个 api key 是否正确

发送国内短信申请签名,短信模板

新建签名(需要审核),新建模板

国内短信api文档: https://www.yunpian.com/doc/zh_CN/domestic/list.html

新建 utils/yunpian.py

线上部署时一定要将自己服务器的ip加入ip白名单中。测试时搜索本机ip地址。

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):
        params = {
            "apikey": self.api_key,
            "mobile": mobile,
            "text": "【云片网】您的验证码是{code}".format(code=code)
        }

        response = requests.post(self.single_send_url, data=params)
        re_dict = json.loads(response.text)
        # print(re_dict)
        return re_dict


# if __name__ == "__main__":
#     yun_pian = YunPian("****02cd03158****bed50****") #api key的值
#     yun_pian.send_sms("2018", "157****4045") #手机号码

注意text内容必须要与后台已申请过签名并审核通过的模板保持一致,
我这里是测试阶段,没有审核,可以使用云片网的默认验证码格式进行测试。

drf 实现发送验证码接口

# 发送验证码是创建model中一条记录的操作
from rest_framework.mixins import CreateModelMixin

用户传过来的手机号码我们要进行两次验证:
1、是否有效
2、是否被注册过

这个验证我们把它放到 Serializer 里面来做
为什么不像 goods 中一样使用 serializers.ModelSerializer
因为我们 model 中的 code 也是必填项,而我们拥有的只有手机号,所以会导致验证失败

setting.py中

# 手机号码正则表达式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"

users/serializers.py

class SmsSerializer(serializers.Serializer):
    mobile = serializers.CharField(max_length=11)

    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("手机号非法")

        # 验证码发送频率, 这里设置为60s
        one_minutes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
        # 验证码频率的时间判断, add_time的时间大于60s
        if VerifyCode.objects.filter(add_time__gt=one_minutes_ago, mobile=mobile).count():
            raise serializers.ValidationError("距离上一次发送未超过60s")

        return mobile

然后 views 中重写 CreateModelMixin 中的 create 方法

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)

    def create(self, request, *args, **kwargs):
        """
        重写CreateModelMixin的create方法
        """
        # serializer这两个配置直接使用CreateModelMixin的create()的
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        mobile = serializer.validated_data["mobile"]
        yun_pian = YunPian(API_KEY)

        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)

serializer.is_valid(raise_exception=True) 有效性验证失败会直接抛异常。
被 drf 捕捉到返回400状态码。

其中的APIKEY需要我们添加到setting.py中

# 云片网设置
APIKEY = 'apikey值'

调试之前配置好对应的 url

from users.views import SmsCodeViewset

# 配置验证码的路由
router.register(r'code', SmsCodeViewSet, base_name="code")

地址:http://127.0.0.1:8000/code/

user serializer和validator验证(注册)

注册页面需要我们输入手机号码,验证码,密码
Django 的form 和 model form 是用来验证用户提交的字段的合法性
restful API 中实际是对资源的操作,注册对应的资源就是用户

因为用户注册必定是会create model 操作的,所以继承 CreateModelMixin
先为 viewset 准备一个配套的 Serializers

users/serializers.py(继承modelSerializer,因为字段都是必填项,都有的,虽然相比较用户model多了一个code字段)

通过一些小技巧,既能享受model Serializer带来的好处,又能突破它的限制

class UserRegSerializer(serializers.ModelSerializer):
    """
    用户注册的序列类
    """
    code = serializers.CharField(required=True, max_length=4, min_length=4,
                                 error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码:",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 },
                                 label="验证码",
                                 write_only=True,
                                 help_text="验证码")

验证 username 是否存在

    username = serializers.CharField(required=True, allow_blank=False, label="手机号码",
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

    password = serializers.CharField(
        style={'input_type': 'password'}, label="密码", write_only=True,
    )

在Serializer中添加code字段,这个code是多余字段不会被保存到数据库中

def validate_code(self, code):

        # 验证码在数据库中是否存在,用户从前端post过来的值都会放入initial_data里面,排序(最新一条)。
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            # 获取到最新一条
            last_record = verify_records[0]

            # 有效期为五分钟。
            five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError("验证码过期")

            if last_record.code != code:
                raise serializers.ValidationError("验证码错误")

        else:
            raise serializers.ValidationError("验证码错误")

验证完之后将code这个字段删除掉

    def validate(self, attrs):
    	# 不加字段名的验证器作用于所有字段之上。attrs是字段 validate之后返回的总的dict
        attrs["mobile"] = attrs["username"]
        del attrs["code"]
        return attrs

views中的用户viewset中实例化Serializer

class UserViewset(CreateModelMixin, viewsets.GenericViewSet):
    """
    用户
    """
    serializer_class = UserRegSerializer

配置url

# 配置用户登录路由
router.register(r'users', UserViewSet, base_name="users")

django信号量实现用户密码修改

用户注册逻辑编码。
后台添加一条用户短信验证码数据之后进行验证。

参考:官方文档

http://www.django-rest-framework.org/api-guide/fields/

我们 Serializer 配置的

        fields = ("username", "code", "mobile", "password")

在 viewset 处理验证过程中已经删除了其中的code。

解决方案(重要参数):
code 字段添加 write only=true。就不会将此字段进行序列化返回给前端。

如果是一个正常的Serializer会将我们post过去的数据序列化之后返回回来。
password也被返回了回来。这是不合理的,为password也添加write only =True参数

密码会被明文存储。

def create(self, validated_data):
        user = super(UserRegSerializer, self).create(validated_data=validated_data)
        user.set_password(validated_data["password"])
        user.save()
        return user

重载Serializer的create方法。可以实现
虽然重载代码量已经很少了,但是可能比较难理解,所以我们选择其他解决方案

django信号量机制

我们的model对象进行操作的时候,会发出全局的信号。捕捉之后做出我们自己的操作。

参考文档
https://docs.djangoproject.com/en/2.0/ref/signals/

django的信号量如request_started和scrapy中
删除之前做一些事情,就可以接收pre_delete的信号

http://www.django-rest-framework.org/api-guide/authentication/

新建文件users/signals.py

# coding:utf-8 
# author: Evan

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model

User = get_user_model()

"""
    参数一接收哪种方式的信号,参数二接受哪个model的信号
"""
@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
    """
    django信号量的简单应用:遵循原来加密方式对密码加密
    """

    """ 新建,update的时候也会进行post_save """
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()

做完刚才这些操作,还要重载一个配置。否则会导致虽然没有任何报错信息,但是密码并没有被加密

apps.py中


class UsersConfig(AppConfig):
    name = 'users'
    verbose_name = '用户管理'

    def ready(self):
        # 必须给django信号量配置就绪函数
        import users.signals

我们修改的mobile的字段可为空,只在代码中修改是没有用的,还需要我们进行migrations
这样我们才可以在后台中不需要mobile字段直接添加用户。

如果是让用户自己去登录,那么就将cookie.setcookie这两行注释掉。直接让它跳转到首页。

但是如果是自动登录,那么我们此时就没有给前台返回jwt 的token。

重载createmodelmixin里面的create函数。

框架原本实现代码:

def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

这里的perform中的save是save了当前的model(user)。但是并没有返回该model。我们要想获取到user model 就必须重写让它返回model

然后在执行perform_create之后插入我们自己的逻辑。

分析 jwt 的源码实现,找到它哪部分才是生成token的。

obtain_jwt_token = ObtainJSONWebToken.as_view()
class ObtainJSONWebToken(JSONWebTokenAPIView):
    """
    API View that receives a POST with a user's username and password.

    Returns a JSON Web Token that can be used for authenticated requests.
    """
    serializer_class = JSONWebTokenSerializer
    

此时我们就可以去查看继承的父类: JSONWebTokenAPIView
该类中用户在post数据过来之后。

def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

关键在于token是直接从Serializer中获取的,那么token的生成应该是在Serializer中实现的。

rest_framework_jwt/settings.py

DEFAULTS = {
    'JWT_ENCODE_HANDLER':
    'rest_framework_jwt.utils.jwt_encode_handler',
      'JWT_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_payload_handler',
    }

所以我们已经找到了生成token的两个重要步骤,一payload,二encode
实现代码:

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

        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)

其中的token要和前端保持一致。注意将原本返回的Serializer.data进行加工之后返回。

退出功能

退出功能就变得更好做了,因为jwt的token并不是保存在服务器端的。

src/views/head/shophead.vue

      <a @click="loginOut">退出</a>

退出按钮调用的是loginout函数

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

清空token给axios发一个通知。跳转到登录页面。

注册页面测试,将code与前端保持一致,以及修改localhost

UserViewSet类如下:

class UserViewSet(CreateModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    """
    用户
    """

    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    # 登录验证
    authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

    # 设置动态序列化
    def get_serializer_class(self):
        if self.action == "retrieve":
            return UserDetailSerializer
        elif self.action == "create":
            return UserRegSerializer
        return UserDetailSerializer

    # 设置动态权限配置, 用户注册无需判断登录状态
    def get_permissions(self):
        if self.action == "retrieve":
            return [permissions.IsAuthenticated()]
        elif self.action == "create":
            return []
        return []

    def create(self, request, *args, **kwargs):
        # 重写CreateModelMixin的create()方法

        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)
        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):
        serializer.save()

猜你喜欢

转载自blog.csdn.net/Yuyh131/article/details/82927591