第一步: 前期分析
可见, 前端会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认证需要的头
--- 君子处其实,不处其华;治其内,不治其外 张居正 ----