1. 程式人生 > >(專案)生鮮超市(六)

(專案)生鮮超市(六)

七、使用者登入與手機註冊

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):
41         # 驗證手機號碼
42         serializer = self.get_serializer(data=request.data)
43         serializer.is_valid(raise_exception=True)
44 
45         # 獲取列印驗證碼
46         mobile = serializer.validated_data["mobile"]
47         code = self.generate_code()
48 
49         code_record = VerifyCode(code=code, mobile=mobile)
50         code_record.save()
51         return Response({
52             "mobile": mobile
53         }, status=status.HTTP_201_CREATED)