用戶的登陸時經過 手機號也能夠進行登陸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")