1. 程式人生 > >Django REST framework+Vue 打造生鮮電商項目(筆記四)

Django REST framework+Vue 打造生鮮電商項目(筆記四)

相關 部署 www. requests ted 本地ip hand ice 用戶信息

(PS:部分代碼和圖片來自博客:http://www.cnblogs.com/derek1184405959/p/8813641.html。有增刪)

一、用戶登錄和手機註冊

1、drf的token功能

(前言:為什麽有了session了,還要用token呢?因為每次認證用戶發起請求時,服務器需要去創建一個記錄來存儲信息。當越來越多的用戶發請求時,內存的開銷也會不斷增加。)
之前用django做的網站登錄都會加上csrf-token防止跨站攻擊,但此次的項目是前後端分離的,而且用戶可以選擇在手機上登陸,就不能限制跨站登陸了。而一開始我們創建超級用戶後,為什麽可以登錄drf?是因為我們在urls.py配置了"path(‘api-auth/‘, include(‘rest_framework.urls‘)),",點進去查看源碼,如下:

技術分享圖片

可以知道drf用的登陸還是基於csrf的模式,因此這裏就不合適我們這個前後端分離的項目。因此我們需要用其他的用戶認證模式,這些在drf的官方文檔裏都有說明,drf提供給我們的auth有三種。

首先在setting.py中配置(不填寫也會默認配置)

REST_FRAMEWORK = {
    DEFAULT_AUTHENTICATION_CLASSES: (
        rest_framework.authentication.BasicAuthentication,
        rest_framework.authentication.SessionAuthentication
,#瀏覽器中常用這種機制,因為瀏覽器會自動生成cookie和session返回給後端。但在前後端分離系統中很少用這個 ) }

2、開始配置token

(1)INSTALL_APP中添加

INSTALLED_APPS = (
    ...
    rest_framework.authtoken
)

token會生成一張表authtoken_token,所以要運行migrations和migrate

技術分享圖片

表的作用:我們現在用的是token的認證模式,在這張表中,只要創建了一個新的用戶,就會生成一個與用戶對應的token

(2)url配置

from rest_framework.authtoken import
views urlpatterns = [ # token path(api-token-auth/, views.obtain_auth_token) ]

(3)這時我們可以用瀏覽器Firefox,安裝個插件Httprequest,來模仿登陸,測試一下這個接口

如圖,輸入接口url,在下方用json格式寫上我們要註冊的用戶名和密碼,用POST方式,就可獲得生成返回的token。

技術分享圖片

token值會保存到數據中,跟這個用戶相關聯

技術分享圖片

(4)客戶端身份驗證

在後端生成token後,並返回給前端,接下來就是前端要怎麽利用這個token值了。(就是帶上token值,模擬用戶登錄)

Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b # 註意Token後面的空格

測試方法就是點擊插件的Headers,按上面我寫的那樣要求,填入數據。

我們要把token放在header裏面,其中key為"Authorization","Token"和它的值為value。但會發現debug模式下返回的用戶信息為空

因為現在我們用到了token的認證方式,如果想獲取用戶信息,就還要在setting.py裏配置一下:

REST_FRAMEWORK = {
    DEFAULT_AUTHENTICATION_CLASSES: (
        rest_framework.authentication.BasicAuthentication,
        rest_framework.authentication.SessionAuthentication,
        rest_framework.authentication.TokenAuthentication‘ # 新增
    )
}

這些配置的作用和setting.py裏面默認配置的

MIDDLEWARE = [
     django.contrib.sessions.middleware.SessionMiddleware,
     django.contrib.auth.middleware.AuthenticationMiddleware,
     ....
]

一樣,當request映射到views.py之前,django和drf會自動調用"SessionAuthentication"或"TokenAuthentication"等這些類裏面的authenticate方法,這種方法會把User放到request當中去。這三種方法(指的是"BasicAuthentication"、"SessionAuthentication"、"TokenAuthentication")是用戶不同類別的驗證,而且會逐一驗證,只要任意一種方法獲取到User,都會放到request裏面。

(小Tips:前端傳到後端的數據,都是保存在data裏面的,但token是保存在auth裏面,如果想要處理前端傳回來的token,就用request.auth取出來)

咳咳,接下來進入源(zhuang)碼(bi)分(bu)析(fen),不感興趣的可以跳過這一部分。

首先為什麽我們在header裏面設置了token之後,通過request就可以取到User了呢?先來分析一波django從請求到相應的過程。根據django源碼"site-packages/django/contrib/session/middleware.py",如圖:

技術分享圖片

其中,在request響應到view之前,settings.py裏面的MIDDLEWARE = [...]所有的xxxMiddleware方法都會去重載這兩個方法(圖中我圈出來那個),當然每個方法放在不同的包裏面,但方法名一樣。現在我們只來分析SessionMiddleware,根據圖中代碼中的"def process_request(self, request)"方法,我們知道它的作用就是把session放到request裏面而已。

(推薦下關於django從請求到響應的過程的文章:http://projectsedu.com/2016/10/17/django從請求到返回都經歷了什麽/)

這種“請求到達Request Middlewares,中間件對request做一些預處理或者直接response請求”的好處有比如我們可以自定義只有特定的瀏覽器才可以訪問,作為全局攔截器,我們可以自定義一個Middlewares,然後在“def process_request”方法裏判斷該是否為chorm瀏覽器,如果不是就返回一個HttpResponce,這樣就不會進入view裏面。

總的來說,就是通過"def process_request"攔截cookies,放到request.session裏面。接下來還有一步,就是通過“contrib/auth/middleware.py”,如圖:

技術分享圖片

以上這些是django的MIDDLEWARE = []中的驗證。

實際上MIDDLEWARE = []和REST_FRAMEWORK = {}中的驗證不一樣,MIDDLEWARE會對每一個request都做一個處理,而REST_FRAMEWORK中的‘DEFAULT_AUTHENTICATION_CLASSES‘是用來驗證用戶信息的。

接下來說說token,為什麽在header加上token就可以把User取出來呢?drf裏面的"rest_framework/authentication.py"的源碼如下:技術分享圖片

技術分享圖片

經過一系列判斷處理之後,會拿到token值,然後傳給“def authenticate_credentials(self, key)”方法

技術分享圖片

技術分享圖片

但是我們最開始登陸註冊的時候,這張表是空表,那這個token是如何填充進來的呢?這時要從urls.py裏面去看

技術分享圖片

點進去查看,

技術分享圖片

以上,就是token實現過程的流程。

drf的token缺點

  • 保存在數據庫中,如果是一個分布式的系統,就非常麻煩
  • token永久有效,沒有過期時間。(當然可以設定有效時間)

(這裏還有個小坑,就是我們配置token是在全局的情況下的。當token無效或者填錯的時候,會返回一個401的錯誤,本來這個是沒什麽問題的,但是當我們訪問的是商品的列表頁,這種是用戶在登陸和未登錄的情況下都可以訪問的,如果這時我們因為token過期無效等情況而返回401錯誤會顯得很奇怪。這個問題後端的解決辦法可以是取消全局配置,然後在view裏面導入from rest_framework.authentication import TokenAuthentication 在需要進行token認證的class裏面寫上 authentication_classes = (TokenAuthentication, ))

二、json web token(jwt)方式完成用戶認證

(jwt介紹:https://www.jianshu.com/p/180a870a308a、https://ruiming.me/authentication-of-frontend-backend-separate-application/)1、安裝與使用

1、jwt的基本使用

(1)安裝

pip install djangorestframework-jwt

(2)使用

REST_FRAMEWORK = {
    DEFAULT_AUTHENTICATION_CLASSES: (
        rest_framework.authentication.BasicAuthentication,
        rest_framework.authentication.SessionAuthentication,
        rest_framework_jwt.authentication.JSONWebTokenAuthentication,
    )
}

(3)url

# jwt的token認證接口
path(jwt-auth/, obtain_jwt_token )

(4)Httprequest

技術分享圖片

Now in order to access protected api urls you must include the Authorization: JWT <your_token> header.

$ curl -H "Authorization: JWT <your_token>" http://localhost:8000/protected-url/

2、vue和jwt接口調試

vue中登錄接口是login

//登錄
export const login = params => {
  return axios.post(`${local_host}/login/`, params)
}

後臺的接口跟前端要一致

urlpatterns = [
    # jwt的認證接口
    path(login/, obtain_jwt_token )
]

現在就可以登錄了

技術分享圖片

jwt接口它默認采用的是Django的auth進行用戶名和密碼登錄驗證,如果用手機號碼登錄的話,就會驗證失敗,因為默認只可以驗證username和password,所以我們需要自定義一個用戶驗證,讓它可以滿足手機號碼驗證等需求

自定義用戶認證

(1)settings中配置

AUTHENTICATION_BACKENDS = (
    users.views.CustomBackend,

)

(2)users/views.py

# users.views.py

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q

User = get_user_model()

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

(3)JWT有效時間設置

settings中配置

import datetime
#有效期限
JWT_AUTH = {
    JWT_EXPIRATION_DELTA: datetime.timedelta(days=7),    #也可以設置seconds=20
    JWT_AUTH_HEADER_PREFIX: JWT,                       #JWT跟前端保持一致,比如“token”這裏設置成JWT
}

3、雲片網發送短信驗證碼

(1)註冊

“開發認證”-->>“簽名管理”-->>“模板管理”

還要添加iP白名單,測試就用本地ip,部署的時候一定要換成服務器的ip

(2)發送驗證碼

apps下新建utils文件夾。再新建yunpian.py,代碼如下:

# apps/utils/yunpian.py

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__":
    #例如:9b11127a9701975c734b8aee81ee3526
    yun_pian = YunPian("2e87d1xxxxxx7d4bxxxx1608f7c6da23exxxxx2")
    yun_pian.send_sms("2018", "手機號碼")

4、drf實現發送短信驗證碼接口

手機號驗證:

  • 是否合法
  • 是否已經註冊

(1)settings.py

# 手機號碼正則表達式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"

(2)users下新建serializers.py,代碼如下:

註意,這裏對手機號碼的驗證代碼中,為什麽SmsSerializer不直接用ModelSerializer繼承 VerifyCode呢?是因為在 VerifyCode裏面code是必填字段,而我們這裏只對mobile

進行驗證

# users/serializers.py

import re
from datetime import datetime, timedelta
from MxShop.settings import REGEX_MOBILE
from users.models import VerifyCode
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()


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("手機號碼非法")

        # 驗證碼發送頻率
        #60s內只能發送一次
        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

(3)APIKEY加到settings裏面

#雲片網APIKEY
APIKEY = "xxxxx327d4be01608xxxxxxxxxx"

(4)views後臺邏輯

我們要重寫CreateModelMixin的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 {}

需要加上自己的邏輯

users/views.py

from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from .serializers import SmsSerializer
from rest_framework.response import Response
from rest_framework import status
from utils.yunpian import YunPian
from MxShop.settings import APIKEY
from random import choice
from .models import VerifyCode

# 對VerifyCode的操作,如發送一條驗證碼就把手機號碼和驗證碼保存在表裏,相當於
# create操作,所以要繼承CreateModelMixin
class SmsCodeViewset(CreateModelMixin,viewsets.GenericViewSet): ‘‘‘ 手機驗證碼 ‘‘‘ serializer_class = SmsSerializer # "serializer_class"是固定的,這樣寫,結合下面的"def create()"方法,就會自動進行驗證 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): 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)

雲片網單條短信發送的使用說明:

技術分享圖片

技術分享圖片

(5)配置url

from users.views import SmsCodeViewset

# 配置codes的url
router.register(rcode, SmsCodeViewset, base_name="code")

開始驗證

技術分享圖片

5、user serializer 和validator驗證

完成註冊的接口

技術分享圖片

(1)修改UserProfile中mobile字段

mobile = models.CharField("電話",max_length=11,null=True, blank=True)

設置允許為空,因為前端只有一個值,是username,所以mobile可以為空

(2)users/serializers.py

註意:這裏使用的是“ModelSerializer”,跟上面的“SmsSerializer”繼承“Serializers”不一樣,雖然UserProfile裏面也沒有code字段,但這裏主要介紹的是當使用“ModelSerializer”時的解決辦法

class UserRegSerializer(serializers.ModelSerializer):
    ‘‘‘
    用戶註冊
    ‘‘‘
    #UserProfile中沒有code字段,這裏需要自定義一個code序列化字段
    code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4,
                                 error_messages={
                                        "blank": "請輸入驗證碼",
                                        "required": "請輸入驗證碼",
                                        "max_length": "驗證碼格式錯誤",
                                        "min_length": "驗證碼格式錯誤"
                                 },
                                help_text="驗證碼")
    #驗證用戶名是否存在
    username = serializers.CharField(label="用戶名", help_text="用戶名", required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(), message="用戶已經存在")])

    #驗證code
    def validate_code(self, code):
        # 用戶註冊,已post方式提交註冊信息,post的數據都保存在initial_data裏面
        #username就是用戶註冊的手機號,驗證碼按添加時間倒序排序,為了後面驗證過期,錯誤等
        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("驗證碼錯誤")

        # 所有字段。attrs是字段驗證合法之後返回的總的dict
    def validate(self, attrs):
        #前端沒有傳mobile值到後端,這裏添加進來
        attrs["mobile"] = attrs["username"]
        #code是自己添加得,數據庫中並沒有這個字段,驗證完就刪除掉
        del attrs["code"]
        return attrs

    class Meta:
        model = User
        fields = (username,code,mobile) # 因為User指的就是Userprofile,而Userprofile繼承自Django的User,所以這裏username是必填字段

(3)users/views.py

class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
    ‘‘‘
    用戶
    ‘‘‘
    serializer_class = UserRegSerializer

(4)配置url

router.register(rusers, UserViewset, base_name="users")

測試代碼:

技術分享圖片

6、django信號量實現用戶密碼修改

(1)完善用戶註冊

user/views.py

class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
    ‘‘‘
    用戶
    ‘‘‘
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

user/serializer.py添加

fields = (username,code,mobile,password)

(2)password不能明文顯示和加密保存

需要重載Create方法

# “write_only=True”,不會返回到前端
password = serializers.CharField(
    style={input_type: password},label=“密碼”,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

當然,上面的需要重載Create方法然後對密碼加密保存我們可以不寫在UserRegSerializer裏面,而是引入另一種方式,即信號量

2、信號量

(1)users下面創建signals.py

下面代碼簡單的解釋就是監聽User是否接收到以post方法傳遞過來的數據,如果有,判斷是否是新創建用戶,如果是,進行保存

# users/signals.py

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


# post_save:接收信號的方式
#sender: 接收信號的model
@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
    # 是否新建,因為update的時候也會進行post_save
    if created:
        password = instance.password
        #instance相當於user
        instance.set_password(password)
        instance.save()

(2)還需要重載配置

users/apps.py

# users/apps.py

from django.apps import AppConfig

class UsersConfig(AppConfig):
    name = users
    verbose_name = "用戶管理"

    def ready(self):
        import users.signals

AppConfig自定義的函數,會在django啟動時被運行

現在添加用戶的時候,密碼就會自動加密存儲了

7、vue和註冊功能聯調

前端頁面註冊後,一般有兩種模式,一種是跳轉到登陸頁面由用戶自己填寫登陸用戶名和密碼,一種是自動幫用戶登錄。下面介紹第二種方法的實現。

首先這裏前端已經寫好了登陸邏輯,不用我們管,我們只需要提供一個“token”接口給前端就行,實現自動登錄

生成token的兩個重要步驟,一是payload,二是encode

class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
    ‘‘‘
    用戶
    ‘‘‘
    serializer_class = UserRegSerializer
    queryset = User.objects.all()

    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 # 默認的create方法,因為返回的是serializer.data,所以把token放到裏面,再返回
        payload = jwt_payload_handler(user) # 這種生成token的方法是從jwt的源碼裏找到的
        re_dict["token"] = jwt_encode_handler(payload) # 生成token
        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 perform_create(self, serializer): # 重載這個函數,因為源碼裏面是沒有返回的,但是我們又需要調用。註意這裏的serializer指的就是
        return serializer.save()          # UserRegSerializer裏的model = User對象

後續的登陸成功後的退出,因為token是保存在客戶端的,所以只需要在前端那裏刪除用戶本地的cookie即可。

Django REST framework+Vue 打造生鮮電商項目(筆記四)