1.用户登录

自定义登录验证逻辑

本项目中用户登录既可以是用户名,也可以是手机号,所以需要重写登录验证逻辑

class CustomBackend(ModelBackend):
    """
    自定义用户验证
    """
    def authenticate(self, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(Q(username=username)|Q(mobile=username))
            if user.check_password(password):
                return user
        except Exception as e:
            return None
登录API
# 配置jwt路由
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    # jwt的认证接口
    path('login/', obtain_jwt_token),
]
登录逻辑

利用jwt生成token实现登录,返回生成的token。

2.用户注册

用户注册基于 手机注册
发送验证码基于 榛子云网
相关链接:

  1. 榛子云网址
  2. 示例
  3. 接口文档

验证码逻辑

验证码API
from users.views import SmsViewSet
from rest_framework.routers import DefaultRouter

# Create a router and register our viewsets with it.
router = DefaultRouter()

#配置手机短信验证码url
router.register(r'codes', SmsViewSet, base_name="codes")
验证码序列化组件

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

  1. 是否注册
  2. 是否合法
  3. 频率限制
import re
import datetime

from django.contrib.auth import get_user_model
from rest_framework import serializers

from MxShop.settings import REGEX_MOBILE
from .models import VerifyCode

User = get_user_model()


# 为啥不用serializers.ModelSerializer,因为短信验证码模型有code和mobile两个字段
# 而注册的时候只有手机号,用ModelSerializer就会导致验证失败
class SmsSerializer(serializers.Serializer):
    """
    短信验证码验证
    """
    mobile = serializers.CharField(max_length=11)

    def validated_mobile(self, mobile):
        """
        验证手机号码,函数名规则为:validated_字段名()
        :return:
        """
        # 验证手机号是否已经注册
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("用户已经存在")

        # 验证手机号是否合法
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("手机号码非法")


        # 验证手机号是否为一分钟前验证过了(防止恶意注册)
        one_min_ago = datetime.datetime.now() - datetime.timedelta(minutes=1)
        if VerifyCode.objects.filter(add_time__gt=one_min_ago, mobile=mobile).count():
            raise serializers.ValidationError("距离上一次发送未超过60s")

        return mobile
验证码视图逻辑处理

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

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

from random import choice

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from django.shortcuts import render
from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status

from .serializer import SmsSerializer
from MxShop.settings import APIKEY, AppId, AppSecret
from utils.zhenzi import ZhenziSmsClient
from .models import VerifyCode


class SmsViewSet(CreateModelMixin, viewsets.GenericViewSet):
    """
    发送短信验证码
    """
    serializer_class = SmsSerializer

    def random_generate_code(self):
        """
        随机生成四位数验证啊
        :return:
        """
        seeds = '1234567890'
        random_str = []
        for i in range(4):
            random_str.append(choice(seeds))
        code = ''.join(random_str)
        return code


    def create(self, request, *args, **kwargs):
        """
        重写create方法
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        serializer = self.get_serializer(data=request.data)
        # raise_exception=True代表验证失败报异常
        serializer.is_valid(raise_exception=True)
        # 取出手机号(验证成功才会执行)
        mobile = serializer.validated_data['mobile']
        # # 随机生成四位数验证码
        code = self.random_generate_code()
        zhen_zi = ZhenziSmsClient("https://sms_developer.zhenzikj.com/", AppId, AppSecret)
        send_status = zhen_zi.send(message="您的验证码是: {}。如非本人操作,请忽略本短信".format(code), number=mobile)
        send_status = eval(send_status)
        if send_status['code'] != 0:
            # 发送失败
            return Response({
                'mobile': send_status['data']
            }, status=status.HTTP_400_BAD_REQUEST )
        else:
            # 发送成功
            # 只有在发送成功时才能保存数据
            verify = VerifyCode(mobile=mobile, code=code)
            verify.save()

            return Response({
                'mobile': send_status['data']
            }, status=status.HTTP_201_CREATED)
榛子云验证码工具
# -*- coding: utf-8 -*-
__author__ = 'kevin'
__date__ = '2019/6/24 11:10'
import urllib.request
import urllib.parse
import ssl


class ZhenziSmsClient(object):
    def __init__(self, apiUrl, appId, appSecret):
        self.apiUrl = apiUrl
        self.appId = appId
        self.appSecret = appSecret

    def send(self, number, message, messageId='', ):
        data = {
            'appId': self.appId,
            'appSecret': self.appSecret,
            'number': number,
            'messageId': messageId,
            'message': message
        }

        data = urllib.parse.urlencode(data).encode('utf-8')
        ssl._create_default_https_context = ssl._create_unverified_context
        req = urllib.request.Request(self.apiUrl + '/sms/send.do', data=data)
        res_data = urllib.request.urlopen(req)
        res = res_data.read()
        res = res.decode('utf-8')
        return res
榛子云相关配置
# 榛子云网配置
AppId = 我的应用的AppId 
AppSecret = '我的应用的AppSecret '

注册逻辑

用户注册API
# 配置用户注册url
router.register(r'register', UserViewSet, base_name="users")
注册序列化组件

选取序列化方式的时候以为是全部的字段都需要用上, 所以要用到 ModelSerializer
前端传过来的所有的数据都在, initial_data 字典里面
验证个别字段方法名必须是:validate_ + 字段名
code字段相关验证:

  1. 是否存在
  2. 是否过期
  3. 是否和数据库中相同

为啥使用filter方法查询验证码,而不用get方法呢?

  1. 如果使用 get 方式需要处理两个异常, 分别是查找到多个信息的情况以及查询到0信息的情况的异常
  2. 但是使用 filter 方式查到多个就以列表方式返回, 如果查询不到数据就会返回空值, 各方面都很方便

字段属性说明:

  1. write_only - 只读字段包含在API输出中,但在创建或更新操作期间不应包含在输入中。 任何错误包含在序列化程序输入中的’read_only’字段都将被忽略。将此属性设置为True可确保在序列化表示时使用该字段,但在反序列化期间创建或更新实例时不使用该字段。
  2. required - 必填
  3. max_length - 最大长度
  4. min_length - 最小长度
  5. error_messages - 错误类型映射提示(blank-空字段提示 required-必填字段提示 max_length-超长度提示 min_length-过短提示)
  6. style - 更改输入标签显示类型
  7. validators - 可以指明一些默认的约束类
  8. help_text - 帮助提示信息
  9. label - 显示名字
  10. allow_blank - 将此参数设置为True将意味着序列化输出的默认值为null,但并不意味着输入反序列化的默认值
class UserRegisterSerializer(serializers.ModelSerializer):
    """
    用户注册
    """
    # write_only=True: code设置这个参数,防止返回时序列化报错->返回时该属性已经被删除
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, error_messages={
        'max_length': '验证码格式错误',
        'min_length': '验证码格式错误',
        'required': '请输入验证码',
        'blank': '请输入验证码'
    }, 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 validated_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 字典中
        all_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by("-add_time")
        if all_records:
            # 时间倒叙排序后的的第一条就是最新的一条
            last_record = all_records[0]

            # 五分钟前的时间
            five_mintes_ago = datetime.datetime.now() - datetime.timedelta(minutes=5)

            # 验证码过期
            if five_mintes_ago > last_record.add_time:
                raise serializers.ValidationError('验证码过期')

            # 验证码错误
            if code != last_record.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
        # username字段是系统中的字段
        fields = ("username", "code", "mobile")
注册视图逻辑处理

视图主要处理 :

  1. 注册时用户的创建
  2. 重写 create 函数来完成注册后自动登录并跳转到主页面(原因:默认的create方法只能实现用户创建无法实现其他附加)
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
    queryset = User.objects.all()
    serializer_class = UserRegisterSerializer
默认的create方法
class CreateModelMixin(object):
    """
    Create a model instance.
    """
    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()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

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

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

密码加密

方法一 在Serializer类中重写create方法:

# apps/users/serializer.y

# 用户表中的 password 是需要加密后再保存的, 故需要重写一次 create 方法
# 当然也可以不这样做, 这里的操作利用 django 的信号来处理, 详情见 signals.py
  def create(self, validated_data):
      user = super(UserRegisterSerializer, self).create(validated_data=validated_data)
      user.set_password(validated_data["password"])
      user.save()
      return user

方法二 利用django的信号量机制:
名称解释:

  1. created - 是否新建
  2. post_save - Django中的model对象保存后,自动触发
  3. sender - 指定User模型(指定要执行的模型)
  4. instance - 表示保存对象, 在这里是被保存的 user 对象
  5. set_password - 密码加密方法
# apps/users/signals.py

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()

@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()
# apps/users/apps.py

from django.apps import AppConfig


class UsersConfig(AppConfig):
    name = 'users'
    # 后台管理应用显示
    verbose_name = '用户'

    def ready(self):
        import users.signals

JWT流程分析

JWT API
# 配置jwt路由
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    # jwt的认证接口
    path('login/', obtain_jwt_token),
]
类视图
class RefreshJSONWebToken(JSONWebTokenAPIView):
    """
    API View that returns a refreshed token (with new expiration) based on
    existing token

    If 'orig_iat' field (original issued-at-time) is found, will first check
    if it's within expiration window, then copy it to the new token
    """
    serializer_class = RefreshJSONWebTokenSerializer


obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.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
序列化组件实现token生成
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生成
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
      }

生成token用到的方法:

  1. jwt_payload_handler
  2. jwt_encode_handler
具体实现
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
    queryset = User.objects.all()
    serializer_class = UserRegisterSerializer

    # 重写 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(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

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

3.用户退出

由于jwt不是保存在服务器端,而是保存在客服端,所以退出不需要再写一个 logout 接口 。只需要前端进行cookie的清空然后跳转到登录页面或主页面都可以,我这里是跳转到登录页面。

<script>
  import { mapGetters } from 'vuex';
  export default {
    computed: {
      ...mapGetters({
        userInfo:'userInfo'
      })
    },
    methods: {
      loginOut(){
        cookie.delCookie('token');
        cookie.delCookie('name');
        //重新触发store
        //更新store数据
        this.$store.dispatch('setInfo');
        //跳转到登录
        this.$router.push({name: 'login'})
      },
    }
  }
</script>

4.个人中心

个人资料的显示和修改

当我去获取用户详情信息的时候,我们就会发现我们不知道用户的id,这时候我们就可以通过重写get_object(控制retieve/delete)的方法返回当前用户。

def get_object(self):
    """
    由于用户id未知,想要通过id获取用户详细信息不行
    :return: 返回当前用户对象
    """
    return self.request.user

当我们获取当前用户的时候,就出现另外一个问题,用户必须是登录的状态才可以,但是注册的时候用户又没有登录。所以我就可以通过动态的加载权限,具体如下:

  1. 用户注册时,无权限,所有人都可以
  2. 用户的获取、用户的修改时,IsAuthenticate权限
from rest_framework.permissions import IsAuthenticated

def get_permissions(self):
    """
    动态加载权限
    """
    # 只有是viewsets才有action
    if self.action == "retrieve":
        return [IsAuthenticated()]
    elif self.action == "create":
        return []
    # 其它方法必须也要有权限
    return []

加上登录验证

# JSONWebTokenAuthentication --> JWT登录验证
authentication_classes = (JSONWebTokenAuthentication, authentication.SessionAuthentication)

此时我们测试接口就会发现只显示两个字段的值,这是注册的序列化组件的值,所以我们还要实现动态加载序列化组件。

# users/serizalizer.py
class UserDetailSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        # username字段是系统中的字段
        fields = ("name", "gender", "birthday", "email", "mobile")

# users/views.py
def get_serializer_class(self):
  """
   注册和用户同用一个视图,不同serializer
   :return: 序列化组件
   """
   if self.action == "retrieve":
       return UserDetailSerializer
   elif self.action == "create":
       return UserRegisterSerializer
   # 其它方法必须也要有权限
   return UserDetailSerializer

修改个人信息,继承UpdateModelMixin即可

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

参考资料:
DRF 商城项目 - 用户( 登录, 注册,登出,个人中心 ) 逻辑梳理
相关链接:
Django中的信号及其用法
signals官方文档
内置信号文档

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐