【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 方法或使用信号量进行加密处理
  • 云通讯注册成功后需要接入短信,否则会出现无可用签名
posted @ 2020-09-07 22:38  Hubery_Jun  阅读(1287)  评论(0)    收藏  举报