七、用戶登錄與手機註冊 1、drf的token 在INSTALLED_APPS中註冊: 然後遷移資料庫,會生成一張表authtoken_token,存放用戶的token信息: 配置token的url: 然後現在測試發起post請求登錄,我們使用postman工具來發起請求: drf返回的token值 ...
七、用戶登錄與手機註冊
1、drf的token
在INSTALLED_APPS中註冊:
1 INSTALLED_APPS = ( 2 'rest_framework.authtoken' 3 )
然後遷移資料庫,會生成一張表authtoken_token,存放用戶的token信息:
配置token的url:
1 from rest_framework.authtoken import views 2 3 4 urlpatterns = [ 5 path('api-token-auth/', views.obtain_auth_token), # drf-token 6 ]
然後現在測試發起post請求登錄,我們使用postman工具來發起請求:
drf返回的token值會保存到資料庫中並與用戶進行關聯:
然後客戶端需要進行身份驗證,令牌密鑰包含在 Authorization
HTTP header 中。關鍵字應以字元串文字 “Token” 為首碼,用空格分隔兩個字元串。例如:
Authorization: Token 30fc1a3cab2d97a6ab3431d603a0bfc40145785b
通過驗證TokenAuthentication
將提供以下憑據:
- request.user
- request.auth
要想獲取這兩個實例,還要在settings.py中添加以下設置:
1 REST_FRAMEWORK = { 2 'DEFAULT_AUTHENTICATION_CLASSES': ( 3 'rest_framework.authentication.BasicAuthentication', 4 'rest_framework.authentication.SessionAuthentication', 5 'rest_framework.authentication.TokenAuthentication' 6 ) 7 }
drf的token也有很大的缺點:
- token信息是保存在資料庫中的,如果是一個分散式的系統,就比較麻煩
- token永久有效,沒有過期時間
2、json web token方式完成用戶認證(JWT)
在虛擬環境中pip install djangorestframework-jwt
將settings中的REST_FRAMEWORK的TokenAuthentication改成JSONWebTokenAuthentication:
1 REST_FRAMEWORK = { 2 'DEFAULT_AUTHENTICATION_CLASSES': ( 3 'rest_framework.authentication.BasicAuthentication', 4 'rest_framework.authentication.SessionAuthentication', 5 # 'rest_framework.authentication.TokenAuthentication' 6 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 7 ) 8 }
然後修改jwt的url:
1 from rest_framework_jwt.views import obtain_jwt_token 2 3 urlpatterns = [ 4 path('jwt-auth/', obtain_jwt_token ) 5 ]
通過postman發起請求:
3、Vue和JWT介面調試
vue中登錄介面是login:
1 //登錄 2 export const login = params => { 3 return axios.post(`${host}/login/`, params) 4 }
後臺的介面要與前端保持一致:
1 urlpatterns = [ 2 path('login/', obtain_jwt_token ), # jwt-token 3 ]
jwt介面預設採用的是用戶名和密碼登錄驗證,如果用手機登錄的話,就會驗證失敗,所以我們需要自定義一個用戶驗證,在users/view.py中編寫:
1 from django.shortcuts import render 2 from django.contrib.auth.backends import ModelBackend 3 from django.contrib.auth import get_user_model 4 from django.db.models import Q 5 6 # Create your views here. 7 8 9 User = get_user_model() 10 11 12 class CustomBackend(ModelBackend): 13 """jwt自定義用戶驗證""" 14 15 def authenticate(self, request, username=None, password=None, **kwargs): 16 try: 17 user = User.objects.get(Q(username=username) | Q(mobile=username)) 18 if user.check_password(password): 19 return user 20 except Exception as e: 21 return None
然後在setting中配置定義好的類:
1 AUTHENTICATION_BACKENDS = ( 2 'users.views.CustomBackend', 3 )
jwt過期時間的設置,在setting中配置:
# jwt過期時間 JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 也可以設置seconds=20 'JWT_AUTH_HEADER_PREFIX': 'JWT', # JWT跟前端保持一致,比如“token”這裡設置成JWT }
4、雲片網發送簡訊驗證碼
在雲片網進行註冊,完善開發者信息,然後新增簽名和模板,審核通過之後,添加ip白名單,測試的時候使用本地ip,線上部署的時候一定要換成伺服器的ip。
然後編寫發送驗證碼的邏輯,在apps下新建utils文件夾,新建yunpian.py文件:
1 import requests 2 import json 3 4 5 class YunPian(object): 6 def __init__(self, api_key): 7 self.api_key = api_key 8 self.single_send_url = 'https://sms.yunpian.com/v2/sms/single_send.json' 9 10 def send_sms(self, code, mobile): 11 # 向雲片網發起請求的參數 12 parmas = { 13 "apikey": self.api_key, 14 "mobile": mobile, 15 "text": "【倍思樂】您的驗證碼是{code}。如非本人操作,請忽略本簡訊".format(code=code) 16 } 17 18 # 發起請求 19 response = requests.post(self, self.single_send_url, data=parmas) 20 re_dict = json.loads(response.text) 21 return re_dict 22 23 24 # 測試 25 if __name__ == '__main__': 26 yun_pian = YunPian('9b11127a9701975c734b8aee81ee3526') 27 yun_pian.send_sms('2018', '13993601652')
現在開始編寫發送簡訊驗證碼的介面,首先在settings中配置手機號碼的正則表達式:
1 # 手機號碼正則表達式 2 REGEX_MOBILE = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"
然後對手機號碼進行序列化,在users下新建serializers.py:
1 import re 2 from datetime import datetime, timedelta 3 4 from rest_framework import serializers 5 from django.contrib.auth import get_user_model 6 7 from MxShop.settings import REGEX_MOBILE 8 from .models import VerifyCode 9 10 User = get_user_model() 11 12 13 class SmsSerializer(serializers.Serializer): 14 mobile = serializers.CharField(max_length=11) 15 16 # 函數名必須是validate + 驗證的欄位名 17 def validate_mobile(self, mobile): 18 """手機號驗證""" 19 20 # 查詢手機號是否已註冊 21 if User.objects.filter(mobile=mobile).count(): 22 raise serializers.ValidationError('用戶已存在') 23 24 # 驗證手機號碼是否合法 25 if not re.match(REGEX_MOBILE, mobile): 26 raise serializers.ValidationError('手機號碼非法') 27 28 # 限制驗證碼的發送頻率,60秒發送一次 29 one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0) 30 if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count(): 31 raise serializers.ValidationError('距離上一次發送未超過60秒') 32 33 return mobile
將雲片網的apikey配置到settings中:
1 # 雲片網的apikey 2 APIKEY = "xxxxx327d4be01608xxxxxxxxxx"
現在開始完善發送簡訊驗證碼的介面:
1 class SmsCodeViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): 2 """手機驗證碼""" 3 4 serializer_class = SmsSerializer 5 6 # 隨機生成code 7 def generate_code(self): 8 seeds = "1234567890" 9 random_str = [] 10 for i in range(4): 11 random_str.append(choice(seeds)) 12 13 return "".join(random_str) 14 15 # 重寫CreateModelMixin的create方法,加入發送驗證碼的邏輯 16 def create(self, request, *args, **kwargs): 17 # 驗證手機號碼 18 serializer = self.get_serializer(data=request.data) 19 serializer.is_valid(raise_exception=True) 20 21 # 發送驗證碼 22 mobile = serializer.validated_data["mobile"] 23 yun_pian = YunPian(APIKEY) 24 code = self.generate_code() 25 sms_status = yun_pian.send_sms(code=code, mobile=mobile) 26 if sms_status["code"] != 0: # 發送失敗 27 return Response({ 28 "mobile": sms_status["msg"] 29 }, status=status.HTTP_400_BAD_REQUEST) 30 else: 31 code_record = VerifyCode(code=code, mobile=mobile) 32 code_record.save() 33 return Response({ 34 "mobile": mobile 35 }, status=status.HTTP_201_CREATED)
然後註冊url:
1 router.register(r'code', SmsCodeViewSet, base_name='code') # 簡訊驗證碼
現在開是在介面中進行驗證,輸入不合法的手機號:
輸入合法的手機號後,會發送簡訊驗證碼到你的手機。
5、註冊介面編寫
在編寫註冊介面之前,需要修改UserProfile中的mobile欄位為可以為空,因為前端只有一個值,是username,所以mobile可以為空:
1 class UserProfile(AbstractUser): 2 """用戶信息""" 3 4 GENDER_CHOICES = ( 5 ("male", u"男"), 6 ("female", u"女") 7 ) 8 name = models.CharField("姓名", max_length=30, null=True, blank=True) 9 birthday = models.DateField("出生年月", null=True, blank=True) 10 gender = models.CharField("性別", max_length=6, choices=GENDER_CHOICES, default="female") 11 mobile = models.CharField("電話", max_length=11, null=True, blank=True) 12 email = models.EmailField("郵箱", max_length=100, null=True, blank=True) 13 14 class Meta: 15 verbose_name = "用戶信息" 16 verbose_name_plural = verbose_name 17 18 def __str__(self): 19 return self.username
然後編寫用戶註冊的serializer:
1 class UserRegSerializer(serializers.ModelSerializer): 2 # UserProfile中沒有code欄位,這裡需要自定義一個code序列化欄位 3 code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, 4 error_messages={ 5 "blank": "請輸入驗證碼", 6 "required": "請輸入驗證碼", 7 "max_length": "驗證碼格式錯誤", 8 "min_length": "驗證碼格式錯誤" 9 }, 10 help_text="驗證碼") 11 # 驗證用戶名是否存在 12 username = serializers.CharField(label="用戶名", help_text="用戶名", required=True, allow_blank=False, 13 validators=[UniqueValidator(queryset=User.objects.all(), message="用戶已經存在")]) 14 15 # 驗證code 16 def validate_code(self, code): 17 # 用戶註冊,post方式提交註冊信息,post的數據都保存在initial_data裡面 18 # username就是用戶註冊的手機號,驗證碼按添加時間倒序排序,為了後面驗證過期,錯誤等 19 verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time") 20 21 if verify_records: 22 # 最近的一個驗證碼 23 last_record = verify_records[0] 24 # 有效期為五分鐘 25 five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) 26 if five_mintes_ago > last_record.add_time: 27 raise serializers.ValidationError("驗證碼過期") 28 29 if last_record.code != code: 30 raise serializers.ValidationError("驗證碼錯誤") 31 32 else: 33 raise serializers.ValidationError("驗證碼錯誤") 34 35 # 所有欄位。attrs是欄位驗證合法之後返回的總的dict 36 def validate(self, attrs): 37 # 前端沒有傳mobile值到後端,這裡添加進來 38 attrs["mobile"] = attrs["username"] 39 # code是自己添加得,資料庫中並沒有這個欄位,驗證完就刪除掉 40 del attrs["code"] 41 return attrs 42 43 class Meta: 44 model = User 45 fields = ('username', 'code', 'mobile')
然後在views.py中編寫用戶註冊的介面:
1 class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): 2 """用戶註冊""" 3 4 serializer_class = UserRegSerializer
註冊url:
1 router.register(r'users', UserViewSet, base_name='users') # 用戶註冊
然後在介面中進行測試:
6、django信號量實現用戶密碼修改
完善用戶註冊介面:
1 class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): 2 """用戶註冊""" 3 4 serializer_class = UserRegSerializer 5 queryset = User.objects.all()
然後在serializers.py中添加密碼欄位:
1 fields = ('username', 'code', 'mobile', 'password')
需要註意的是密碼不能明文顯示,需要加密保存, 這是重載Create方法:
1 class UserRegSerializer(serializers.ModelSerializer): 2 # UserProfile中沒有code欄位,這裡需要自定義一個code序列化欄位 3 code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, 4 error_messages={ 5 "blank": "請輸入驗證碼", 6 "required": "請輸入驗證碼", 7 "max_length": "驗證碼格式錯誤", 8 "min_length": "驗證碼格式錯誤" 9 }, 10 help_text="驗證碼") 11 # 驗證用戶名是否存在 12 username = serializers.CharField(label="用戶名", help_text="用戶名", required=True, allow_blank=False, 13 validators=[UniqueValidator(queryset=User.objects.all(), message="用戶已經存在")]) 14 15 # 輸入密碼的時候不顯示明文 16 password = serializers.CharField( 17 style={'input_type': 'password'}, label=True, write_only=True 18 ) 19 20 # 密碼加密保存 21 def create(self, validated_data): 22 user = super(UserRegSerializer, self).create(validated_data=validated_data) 23 user.set_password(validated_data["password"]) 24 user.save() 25 return user 26 27 # 驗證code 28 def validate_code(self, code): 29 # 用戶註冊,post方式提交註冊信息,post的數據都保存在initial_data裡面 30 # username就是用戶註冊的手機號,驗證碼按添加時間倒序排序,為了後面驗證過期,錯誤等 31 verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time") 32 33 if verify_records: 34 # 最近的一個驗證碼 35 last_record = verify_records[0] 36 # 有效期為五分鐘 37 five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) 38 if five_mintes_ago > last_record.add_time: 39 raise serializers.ValidationError("驗證碼過期") 40 41 if last_record.code != code: 42 raise serializers.ValidationError("驗證碼錯誤") 43 44 else: 45 raise serializers.ValidationError("驗證碼錯誤") 46 47 # 所有欄位。attrs是欄位驗證合法之後返回的總的dict 48 def validate(self, attrs): 49 # 前端沒有傳mobile值到後端,這裡添加進來 50 attrs["mobile"] = attrs["username"] 51 # code是自己添加得,資料庫中並沒有這個欄位,驗證完就刪除掉 52 del attrs["code"] 53 return attrs 54 55 class Meta: 56 model = User 57 fields = ('username', 'code', 'mobile', 'password')
下麵通過信號量的方式來保存密碼,在users下新建signals.py文件:
1 from django.dispatch import receiver 2 from django.db.models.signals import post_save 3 from django.contrib.auth import get_user_model 4 5 6 User = get_user_model() 7 8 9 # post_save接收信號的方法, sender接收信號的model 10 @receiver(post_save, sender=User) 11 def create_user(sender, instance=None, created=False, **kwargs): 12 # 是否新建,因為update的時候也會進行post_save 13 if created: 14 # instance相當於user 15 password = instance.password 16 instance.set_password(password) 17 instance.save()
然後在users/apps.py中重載配置:
1 from django.apps import AppConfig 2 3 4 class UsersConfig(AppConfig): 5 name = 'users' 6 verbose_name = "用戶管理" 7 8 def ready(self): 9 import users.signals
AppConfig自定義的函數,會在django啟動時被運行,現在添加用戶的時候,密碼就會自動加密存儲了。
7、Vue和註冊介面聯調
完善註冊介面:
1 class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): 2 """用戶註冊""" 3 4 serializer_class = UserRegSerializer 5 queryset = User.objects.all() 6 7 def create(self, request, *args, **kwargs): 8 serializer = self.get_serializer(data=request.data) 9 serializer.is_valid(raise_exception=True) 10 11 user = self.perform_create(serializer) 12 re_dict = serializer.data 13 payload = jwt_payload_handler(user) 14 re_dict["token"] = jwt_encode_handler(payload) 15 re_dict["name"] = user.name if user.name else user.username 16 17 headers = self.get_success_headers(serializer.data) 18 return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers) 19 20 def perform_create(self, serializer): 21 return serializer.save()
然後將Vue中register的介面的host修改:
1 //註冊 2 3 export const register = parmas => { return axios.post(`${host}/users/`, parmas) }
然後在註冊頁面進行測試,發送簡訊註冊成功跳轉到首頁:
如果沒有在雲片網審核通過的童靴想要測試介面是否正確,可以先暫時修改發送簡訊的介面,將隨機生成的驗證碼列印出來,暫時不同雲片網發送簡訊,修改發送簡訊的介面:
1 class SmsCodeViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): 2 """手機驗證碼""" 3 4 serializer_class = SmsSerializer 5 6 # 隨機生成code 7 def generate_code(self): 8 seeds = "1234567890" 9 random_str = [] 10 for i in range(4): 11 random_str.append(choice(seeds)) 12 13 print("".join(random_str)) 14 15 return "".join(random_str) 16 17 # 重寫CreateModelMixin的create方法,加入發送驗證碼的邏輯 18 # def create(self, request, *args, **kwargs): 19 # # 驗證手機號碼 20 # serializer = self.get_serializer(data=request.data) 21 # serializer.is_valid(raise_exception=True) 22 # 23 # # 發送驗證碼 24 # mobile = serializer.validated_data["mobile"] 25 # yun_pian = YunPian(APIKEY) 26 # code = self.generate_code() 27 # sms_status = yun_pian.send_sms(code=code, mobile=mobile) 28 # if sms_status["code"] != 0: # 發送失敗 29 # return Response({ 30 # "mobile": sms_status["msg"] 31 # }, status=status.HTTP_400_BAD_REQUEST) 32 # else: 33 # code_record = VerifyCode(code=code, mobile=mobile) 34 # code_record.save() 35 # return Response({ 36 # "mobile": mobile 37 # }, status=status.HTTP_201_CREATED) 38 39 # 以下為沒有使用雲片網 40 def create(self, request, *args, **kwargs):