(项目)生鲜超市(六)

七、用户登录与手机注册

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)

 

posted @ 2018-11-26 22:46  Sweltering  阅读(482)  评论(0编辑  收藏  举报