(生鲜项目)18. 用户手机号注册 (code验证 + 信号量 + 和前端Vue联调)

vue

第一步: 前期分析

可见, 前端会POST过来三个字段, 所以这里我们应该使用 mixins.CreateModelMixin

由于我们自定义的Userprofile中只有name是必填字段(注意:django自带的user中username是必填字段), 所以我们可以在serializers.py中使用 serializers.ModelSerializer

code的验证需要单独的validate_code()方法

  • 并没有向手机号发送验证码, 用户随便输入的   
  • 有发送过验证码, 但是超过了5分钟过期限制
  • 有发送过验证码, 但是用户输错了
  • 如果发送了多条验证码, 应该只核对用户收到的最新的那个验证码

但是有一个问题, 传过来的code是我们Userprofile中没有的字段, 怎么使用ModelSerializer时将code验证并过滤掉?  答:先增加一个code字段, 然后使用全局钩子删掉code这个字段

注意: 上面这个页面, Vue前端post过来的手机号码放在username属性里的,  所以我们在取手机号码的时候, 应该用来self.initial_data["username"]取, 然后我们自己将其复制到mobile属性中去

这里我们还需要学会自定义错误提示信息 error_messages, 

虽然在发送验证码阶段, 已经验证了手机号码是否存在,但在这里由于前端post的是用户名,所以也要对用户名进行去重验证 UniqueValidator,校验是否已经存在

第二步: 写代码

1.首先写UserSerializer, 专门来处理code的验证, 和Username去重的验证

from rest_framework import serializers

from django.contrib.auth import get_user_model

from rest_framework.validators import UniqueValidator # 给username字段做查重用

User = get_user_model() # 可以获取数据库的userprofile表

# 验证码校验

class UserRegSerializer(serializers.ModelSerializer):

# 由于userprofile并没有code字段,所以先加进来

code = serializers.CharField(required=True, max_length=4, min_length=4, label="短信验证码",

# 自定义的错误提示信息

error_messages={

"blank":"你的验证码哪儿去了", # blank针对的是有字段名,但没有字段值

"required":"请输入验证码", # required针对的是连字段名都没有

"max_length":"验证码长度错误",

"min_length":"验证码长度错误",

},

help_text="验证码")

# 虽然在发送验证码阶段, 已经验证了手机号码是否存在,但在这里由于前端post的是用户名,所以也要对用户名进行验证,校验是否已经存在

username = serializers.CharField(required=True,allow_blank=False, label="用户名",

validators=[UniqueValidator(queryset=User.objects.all(),message="用户已经存在")])

# style可以将密码改成密文格式显示

password = serializers.CharField(

style={\'input_type\': \'password\'},

label="密码"

)

# 局部钩子, 只对code进行校验

def validate_code(self, code):

# POST过来的数据封装在self.initial_data里, 且传递过来手机号的key是username

# 一定要按时间排序,因为我们只会取最后一条code来验证, order_by默认是升序

verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")

# 如果验证码存在

if verify_records:

# 取最新的那一条验证码

last_records = verify_records[0]

# 验证码有效期为5min

five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)

if five_minute_ago > last_records.add_time:

raise serializers.ValidationError("验证码过期")

# 验证码核对

if last_records.code != code:

raise serializers.ValidationError("验证码错误")

# 如果验证码不存在, 则直接报错

else:

raise serializers.ValidationError("验证码错误")

# 全局钩子, 目的是过滤掉code字段(code其实只用于验证,而不用存在userprofile表中)

# attrs是局部钩子清洗后的所有字段的dict

def validate(self, attrs):

attrs["mobile"] = attrs["username"]

del attrs["code"]

return attrs

class Meta:

model = User

fields = ("username", "code", "mobile","password")

然后是views.UserViewset

# 用户注册逻辑

class UserViewset(CreateModelMixin,viewsets.GenericViewSet):

serializer_class=UserRegSerializer

别忘记配置url

# 用户注册接口(包含code验证,username去重验证)

router.register(r\'users\', UserViewset, basename="users")

 2. 最后去浏览器验证

  

第三步: \'UserProfile\' object has no attribute \'code\' 异常处理

1.  现在我们人为往数据库中添加一个手机号和验证码(省钱), 然后尝试着去POST, 最后的结果就是提示userprofile表没有code字段, 前面我们明明已经做的那么好了, 甚至还刻意删掉了code字段, 为什么还是报错???

  

2. 主要问题出在REST自带的 CreateModelMixin 上, 见下图

 3. 解决办法就是给code加一个字段属性, write_only=True,       补充:关于REST的serializers fields  https://www.django-rest-framework.org/api-guide/fields/

code = serializers.CharField(...

write_only=True, # 有了这个就解决了\'UserProfile\' object has no attribute \'code\' 的问题

# 原理就是执行serializer.data时,该字段不会被序列化
...)

4.先去数据库把18572355522这个用户删掉, 再把验证码时间改成现在, 再次测试是否能成功注册, POST页面如下

如果后台逻辑没有问题, 那么你提交的什么字段, 就应该返回什么字段, 见下图, 说明成功了

  

5. 但这个时候又有2个新的问题, 一就是我们不想让密码返回, 二是数据库中居然也存的是明文的密码解决办法见下

    password = serializers.CharField(

     ...

write_only=True, # 让密码不返回

)

# 让密码在保存的时候加密 (拦截create方法, 在其屁股后面加上密码加密)

def create(self, validated_data):

user = super(UserRegSerializer, self).create(validated_data=validated_data)

user.set_password(validated_data["password"])

user.save()

return user

 问题完美解决了!

6. 提问: 难道只有重写create才能达到这个目的吗??? No, 这里就引出django信号量的问题    详见: https://yiyibooks.cn/xx/Django_1.11.6/topics/signals.html

 最常用的是 django.db.models.signals 即在执行数据库操作的时候,Model在实例化前后, 保存前后, 删除前后, 会向全局发送信号, 此时, 我们可以利用这个信号实现功能的补充, 这样做的优点是可移植性非常高.

 该项目中, 只会用到 post_save(), REST中的  Authentication 模块有讲解这个信号量怎么用

除了django内置的信号函数, 我们也可以自定义信号函数,不过一定要记得要将信号发送出去, (内置的信号会自动发送)

 新建signals.py

from django.db.models.signals import post_save

from django.dispatch import receiver

from rest_framework.authtoken.models import Token

from django.contrib.auth import get_user_model

User = get_user_model() # 可以获取数据库的userprofile表

@receiver(post_save, sender=User) # sender即我们接收哪个Model传递过来的

def create_auth_token(sender, instance=None, created=False, **kwargs):

if created: # created即只有当检测到是新建用户时, 才执行以下逻辑

password = instance.password

instance.set_password(password)

instance.save()

# Token.objects.create(user=instance) # 我们已经使用了JWT模式, 就无须再创建Token

千万别忘记还有一个app的设置users.apps.py

from django.apps import AppConfig

class UsersConfig(AppConfig):

name = \'users\'

verbose_name = "用户管理"

def ready(self):

import users.signals

 最后去浏览器POST测试, 信号检测成功, 密码也加密成功

第四步: 前端Vue的注册功能联调

1.  现在分析前端的源码, 前端把手机号码以username形式传过来的, 这和我们前面说的一致, 此外可以看见前端一旦得到username和token就会立即以登录的状态跳转到首页去

2. 要想在后端生成token, 唯一的办法就是重载这个ViewSet的create函数, 但这里我们首要的任务是分析JWT是怎么样产生token的

通过下面的源码可以看到, 生成token的两个函数都封装在jwt框架中的util中, 所以我们只需要把user传给他们, 再接收他们的返回值就可以了

3.重写create, 和 perform_create 

from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

# 用户注册逻辑

class UserViewset(CreateModelMixin, viewsets.GenericViewSet):

serializer_class = UserRegSerializer

# 重写create函数,使得前端注册成功后,后端可以返回该用户的token,好让前端直接处于一个登陆的状态

# 同时,要明白后端是如何根据user生成token的

def create(self, request, *args, **kwargs):

serializer = self.get_serializer(data=request.data)

serializer.is_valid(raise_exception=True)

user = self.perform_create(serializer)

# 给serializer.data封装上token字段,返给前端用的

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)

# 源码中的该函数并没有返回值,而这里我们需要其返回user

def perform_create(self, serializer):

return serializer.save() # 知识点: serializer.save()返回的就是它对应的实例,这里即user

4. 前端测试, 可见注册接口没有问题了

 5. 疑问,我们知道JWT插件允许的格式是 "JWT ###.xxx.$$$", 前端只拿到token是怎么封装的? 见下图, api.index.js 已经把每一个请求都封装上了JWT认证需要的头

---  君子处其实,不处其华;治其内,不治其外   张居正  ----

以上是 (生鲜项目)18. 用户手机号注册 (code验证 + 信号量 + 和前端Vue联调) 的全部内容, 来源链接: utcz.com/z/375126.html

回到顶部