Django REST framework+Vue 打造生鮮電商專案(筆記四)
(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配置
fromrest_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(r'code', 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(r'users', 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即可。