用戶登錄
自定義用戶登錄字段處理
用戶的登錄時通過 手機號也可以進行登錄
需要重寫登錄驗證邏輯
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 接口進入驗證, 調用默認重寫后的驗證邏輯進行處理
url(r'^login/', obtain_jwt_token)
驗證成功后會返回 token

用戶注冊
用戶注冊基於 手機號注冊
驗證碼發送基於 雲片網 提供的技術支持
驗證碼邏輯
驗證碼API 接口
# 配置手機驗證碼發送 的 url router.register(r'codes', SmsCodeViewset, base_name="codes")
驗證碼序列化組件
選取序列化方式的時候以為不是全部的字段都需要用上, 因此不需用到 ModelSerializer
需要對前端拿到的 mobile 字段進行相關的驗證
是否注冊, 是否合法, 以及頻率限制
# 手機驗證序列化組件 # 不使用 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 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"
注冊邏輯
注冊 API 接口
# 配置用戶注冊的 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 方法
定位重寫 create 方法
可見只有序列化類的更新和推送, 無其他功能
默認的 create 方法

如果想實現自動登錄, 首先本質就是加入用戶登錄的狀態, 即 token 的生成和保存
本次項目使用的是 JWT 作為 token 方案, 因此 需要考究在 JWT 的源碼中 token 如何生成
定位 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 我們需要用到這兩個方法, 使用方法就完全模仿源碼即可
完成 create 重寫
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 方式添加
用戶中心的數據來源是對單一用戶的詳細數據請求, 因此需要在原有基礎上加上對 retrieve 的處理
mixins.RetrieveModelMixin
用戶 id 傳遞
同時因為對單一用戶的請求需要指明用戶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")
