【Vue+DRF 生鮮電商】注冊登錄(四)


本章將實現注冊、登錄,包括短信驗證碼在內的三個接口。

1. 登錄

用戶認證的兩種方式:

  • drftoken,保存在數據庫中,如果是分布式系統比較麻煩,且 token 永久有效,無過期時間
  • jwt

1.1 drf token 實現用戶認證

1、settings.py

INSTALLED_APPS = (
    ...
    'rest_framework.authtoken'
)

migrate 生成一張 authtoken_token 數據表,用於存儲每個用戶的 token

2、配置路由:

from rest_framework.authtoken import views

urlpatterns = [
    path('login/', views.obtain_auth_token),    
]

3、xadmin 創建一個新的用戶,訪問 http://127.0.0.1:8000/login,再 post 相應的用戶名和密碼,會返回一個 token

也可以使用 postman、postwoman 等軟件進行接口測試。

4、客戶端身份驗證:

令牌秘鑰包含在 Authorization HTTP header 中,並以 Token 為前綴(關鍵字),關鍵字修改只需子類化 TokenAuthentication,並設置 keyword 類變量:

  • 身份驗證成功,TokenAuthentication 提供一些憑據:

    • request.userDjango User 實例對象
    • request.authrest_framework.authtoken.models.Token 實例對象
  • 身份驗證失敗:響應 HTTP 401 Unauthorized

5、獲取 request.user 和 request.auth,需在 settings 中添加:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication'
    )
}

1.2 jwt 實現用戶認證

官網:http://getblimp.github.io/django-rest-framework-jwt/

1、安裝:

pipenv install djangorestframework-jwt
# pip install djangorestframework-jwt

2、配置 settings

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    )
}

3、路由:

from rest_framework_jwt.views import obtain_jwt_token

path('login/', obtain_jwt_token),    # jwt 的 token 認證

4、接口測試:http://127.0.0.1:8000/login post 請求:

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImhqIiwiZXhwIjoxNTk0MzA5MDEwLCJlbWFpbCI6ImhqQHFxLmNvbSJ9.mcBE_FVwt5pOn2nuM6ynve3scA3i3ThiIdxINrO8oVE"
}

1.3 自定義用戶認證

jwt 默認采用用戶名和密碼進行驗證,要想支持手機驗證,需要自定義驗證。

1、settings.py

AUTHENTICATION_BACKENDS = (
    'users.views.CustomBackend',
)

2、users/views.py

from django.contrib.auth import get_user_model

User = get_user_model()


class CustomBackend(ModelBackend):
    """自定義用戶驗證"""
    def authenticate(self, request, 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 有效期時間:

# jwt 有效期限
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),  # 也可以設置seconds=20
    'JWT_AUTH_HEADER_PREFIX': 'JWT',  # JWT跟前端保持一致,比如“token”這里設置成JWT
}

2. 短信驗證碼

2.1 注冊雲通訊

能夠發送短信驗證碼的雲通訊網站有很多,比如:

  • 雲片網:https://www.yunpian.com/
  • 榮聯運:https://www.yuntongxun.com/

注冊成功后都有提供免費的測試短信(一般為 10 條),這里采用的是雲片網。

新建 Pythonapps/utils,再新建 utils/yunpian.py 文件,用於給雲片網發送請求,最后通過雲片網給相應的手機發送短信驗證碼:

import requests
import json


class YunPian:
    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):
        # 需要傳遞的參數
        params = {
            "apikey": self.api_key,
            "mobile": mobile,
            "text": "【Hubery_Jun 生鮮超市】您的驗證碼是 {code},1 分鍾有效。如非本人操作,請忽略本短信".format(code=code)
        }
        print(params)
        response = requests.post(self.single_send_url, data=params)
        print(response.status_code, response.text)
        re_dict = json.loads(response.text)
        return re_dict


if __name__ == '__main__':
    yun_pian = YunPian("xxx")
    yun_pian.send_sms("2020", "手機號碼")

2.2 短信驗證碼接口

1、添加相應配置 settings

# 手機號碼正則表達式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"

# 雲片網 APIKEY,注冊成功后在控制台可查看
APIKEY = "f84c2dc13c55xxxxx6e783ba65ab"

2、序列化數據(驗證手機號、驗證碼是否合法)users/serializers.py

import re
from datetime import datetime, timedelta

from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.validators import UniqueValidator

from users.models import VerifyCode

from MxShop.settings import REGEX_MOBILE


class SmsSerializer(serializers.Serializer):
    """短信驗證"""
    mobile = serializers.CharField(max_length=11)

    def validate_mobile(self, mobile):
        """
        手機號碼驗證:函數名必須為 validate_驗證字段名
        :param mobile:
        :return:
        """
        # 手機號是否已注冊
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("用戶已存在")

        # 手機號格式是否合法
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError("手機號格式非法")

        # 驗證碼頻率,1min 只能發一次
        one_min_time = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)

        # 從數據庫中取出驗證碼,view 中生成驗證碼並發送成功后,會將驗證碼存儲到 VerifyCode 模型中,這里只需取出驗證時間即可
        if VerifyCode.objects.filter(add_time__gt=one_min_time, mobile=mobile).count():
            raise serializers.ValidationError("距離上一次發送未超過60s")

        return mobile

3、視圖 users/views.py

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)

    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)

        # 這段不需要添加(臨時添加的)
        # code_record = VerifyCode(code=code, mobile=mobile)
        # code_record.save()

        # 短信驗證碼發送失敗
        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)

4、配置路由 MxShop/urls.py

router.register(r'code', SmsCodeViewSet, basename="code")   # 短信驗證碼

5、接口測試:

3. 注冊

3.1 drf 注冊接口實現

前端 vue 中注冊支持用戶名和手機號進行注冊,所以需要添加對手機號的注冊實現。

1、修改模型 UserProfile mobile 字段:

# 修改之前
mobile = models.CharField("電話", max_length=11)

# 修改之后
mobile = models.CharField("電話", max_length=11, null=True, blank=True)

因為注冊用戶名可以是用戶名,也可以是手機號,而模型中 mobile 字段不能為空。

2、用戶注冊序列化 users/serializers.py

class UserSerializer(serializers.ModelSerializer):
    """用戶注冊"""
    # UserProfile 中沒有 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="用戶名已存在")])

    def validate_code(self, code):
        """
        驗證驗證碼
        post 數據都保存在 initial_data 里,username 為用戶注冊的手機號,驗證碼
        按添加時間倒序排序,為了后面驗證過期,錯誤等
        :param code:
        :return:
        self.initial_data:{'password': 'abcd110139', 'username': '18674447633', 'code': '6188'}
        """
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time')


        if verify_records:
            # 最近的一個驗證碼
            last_record = verify_records[0]
            # 有效期為 5 min
            five_mintues_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            if five_mintues_ago > last_record.add_time:
                raise serializers.ValidationError('驗證碼過期!')

            if last_record.code != code:
                raise serializers.ValidationError('驗證碼錯誤!')
        else:
            raise serializers.ValidationError('驗證碼錯誤!')

    def validate(self, attrs):
        """
        驗證所有字段,attr 為驗證合法后返回的 dict
        :param self:
        :param attrs:
        :return:
        """
        # 前端沒有傳 mobile 值到后端,添加進來
        attrs['mobile'] = attrs['username']

        # 模型中沒有 code 字段,驗證完之后,刪除
        del attrs['code']
        return attrs


    class Meta:
        model = User
        # 前端顯示的字段
        fields = ('username', 'code', 'mobile')

主要用於驗證驗證碼 code 字段(需要添加新字段),以及其他字段。

3、users/views.py

from users.serializers import SmsSerializer, UserSerializer


class UserCreateViewSet(CreateModelMixin, viewsets.GenericViewSet):
    """
    創建用戶、注冊用戶
    """
    serializer_class = UserSerializer

4、配置路由 MxShop/urls.py

from users.views import SmsCodeViewSet, UserCreateViewSet

router.register(r'users', UserCreateViewSet, basename='users')   # 注冊

3.2 修改用戶密碼

給注冊接口添加密碼字段,並進行密文存儲(默認明文),有兩種方法:

  • 重新 create() 方法
  • Django 信號量

3.2.1 重寫 create 方法

1、users/views.py:

from users.serializers import SmsSerializer, UserSerializer


class UserCreateViewSet(CreateModelMixin, viewsets.GenericViewSet):
    """
    創建用戶、注冊用戶
    """
    serializer_class = UserSerializer
    queryset = User.objects.all()

2、users/serializers.py

from rest_framework.validators import UniqueValidator

class UserSerializer(serializers.ModelSerializer):
    """用戶注冊"""
    # 添加 password 字段
    password = serializers.CharField(style={'input_type': 'password'}, label="密碼", write_only=True)

    def create(self, validated_data):
        """密碼加密保存"""
        user = super(UserSerializer, self).create(validated_data=validated_data)
        user.set_password(validated_data['password'])
        user.save()
        return user

    # fields 中添加  password 字段
    class Meta:
        model = User
        fields = ('username', 'code', 'mobile', 'password')

3.2.2 信號量

1、新建 users/signals.py

from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver

User = get_user_model()


# post_save:信號的方法
# sender:接收信號的 model
@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
    # 是否新建,update 不需要新建,只需更新
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()  # instance 相當於 user 對象

2、重載配置 users/apps.py

from django.apps import AppConfig


# 項目啟動時運行
class UsersConfig(AppConfig):
    name = 'users'

    # 設置 app 名字為中文,admin 中
    verbose_name = '用戶管理'

    # 新增
    def ready(self):
        import users.signals

3.3 生成 token

生成 token 需要兩個步驟:payload 和 encode

users/views.py

class UserCreateViewSet(CreateModelMixin, viewsets.GenericViewSet):
    """
    創建用戶、注冊用戶
    """
    serializer_class = UserSerializer
    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
        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 perform_create(self, serializer):
        return serializer.save()

至此三個接口均已完成。

總結

  • 不管是登錄還是注冊, token 驗證,盡量采用 jwt
  • 注冊接口密碼默認是明文的,可以重寫 create 方法或使用信號量進行加密處理
  • 雲通訊注冊成功后需要接入短信,否則會出現無可用簽名


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM