(项目)生鲜超市(六)

发布时间:2019-04-10 21:14:17编辑:auto阅读(2267)

    七、用户登录与手机注册

    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)

     

关键字