06-用户登录和手机注册

一、用户登录和手机注册

1、DRF的token登录和原理

  在前后端不分离中,登录需要添加csrf_token,要进行安全验证,但却在前后端分离中,我们不需要进行csrf_token验证,为什么不用验证呢?因为前端是用APP,安卓来写的,因此这一定是跨站验证,因此不用csrf_token验证,但DRF官方文档给了验证用户方式。

 

 因此将这个配置到项目设置中去。

 

完成上面全局配置,现在来进行tokenTokenAuthentication)的认证模式。

 

 

 将这个注册到INSTALLED_APPS中,需要迁移表,因为每注册一个app,都会有对应的表生成。迁移表记录:

 

 

Note: Make sure to run manage.py migrate after changing your settings. The rest_framework.authtoken app provides Django database migrations.


You'll also need to create tokens for your users.上面表迁移完,然后官方文档又说需要为我的用户创建tokens

from rest_framework.authtoken.models import Token

token = Token.objects.create(user=...)
print(token.key)

  这个是我们新建的,但我们想要的是用户在进行注册的时候,数据表中就自动创建好这个token,因此我们可以写一个逻辑,当用户注册保存到UserProfile的时候,会自动调用创建token这个逻辑,并完成token的创建。

When using TokenAuthentication, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the obtain_auth_token view to your URLconf:将我们需要认证的URLconf配置好,让它返回我们的Token给前端,这样前端拿着我们的token就可以进行认证。

from rest_framework.authtoken import views
urlpatterns += [
    url(r'^api-token-auth/', views.obtain_auth_token)
]

 

 配置好了之后,我们来测试我们的接口,测试这个api-token-auth这个接口,需要插件Postman软件进行api请求:

 

后端返回的数据,token密码口令

 

 

  数据表中生成token数据,只要用户第一次通个这个api请求,那么会自动生成token数据保存在数据库中,官方文档告诉我们如何使用这个tokenheader中的键是前面的Authorization,值是后面的。

Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

然后现在用Postman软件的header中添加Authorization键值对,请求,请求来到中间件,每一个中间件都是会对数据进行处理。 要想获取request.userrequest.auth还要在settings中添加:

#配置用户认证
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ]
}

这样就能获取request.userrequest.auth啦,但这样DRFtoken存在缺点:(1)保存在数据库中,如果是一个分布式服务器,会比较麻烦。(2)token永久有效,没有一个过期时间。

2、json web token原理

  前后端分离之JWT用户认证,前后端分离为啥要认证呢,因为HTTP协议是无状态协议,为了不麻烦,因此我们只需要验证用户是否是登录状态,在传统的方式,是利用CookieSeesion来保存用户的状态,浏览器访问将Cookie带上,然后后端验证Cookie就可以通过或者不通过,前后端分离通过RESTful API进行数据交互时,前端登陆,后端根据用户信息生成一个token,并保存token和对应用户id到数据库或Session中,接着把token传给浏览器,存入浏览器的cookie中,之后浏览器请求带上这个Cookie,后端根据这个cookie值来查询用户,验证是否过期。如果这样的话,页面出现XSS漏洞,由于Cookie可以被JS读取,XSS漏洞会导致用户token泄露(token没有过过期时间)。为了不被JS读取,可以设置httponly,设置securecookie就只允许通过HTTPS传输。secure选项可以过滤掉一些使用HTTP协议的XSS注入,但不能完全阻止。如果将验证数据保存到数据中,这大大增加后台的查询和存储开支,若把验证信息保存在Session中,又加大了服务端的存储压力。

3、json web token方式完成用户认证

 pip install djangorestframework-jwt

 settings.py, 中加入 JSONWebTokenAuthentication 到REST_FRAMEWORK DEFAULT_AUTHENTICATION_CLASSES.

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

配置URLconf:

from rest_framework_jwt.views import obtain_jwt_token
#...

urlpatterns = [
    '',
    # ...

    url(r'^api-token-auth/', obtain_jwt_token),
]

利用Postman进行访问

 

 

 

 

Now in order to access protected api urls you must include the Authorization: JWT <your_token> header.

$ curl -H "Authorization: JWT <your_token>" http://localhost:8000/protected-url/

 4、vue和jwt接口调试

 

 

 

 

 

 然后进行登录,注意这里要求输入的是手机号,因此我们先输入用户名,手机号需要在后端中添加验证逻辑。

 

 

 

 

 

 然后发现登录成功啦,接下来,我们需要在后台加上手机号验证逻辑。首先去设置里面设置:

#添加手机验证配置
AUTHENTICATION_BACKENDS = (
    "users.views.CustomBackend",
)

然后来到users.view下编写视图:

from django.shortcuts import render
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q

User = get_user_model()

# Create your views here.

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

这样就将手机验证传入进来啦。我们DEBUG运行项目,来验证我们定义的手机验证能进入来不,登陆发现可以进入我们的逻辑,按下F8,运行项目,发现登录成功。

 

 

 JWT有很多设置,官方如下说明

Additional Settings

There are some additional settings that you can override similar to how you'd do it with Django REST framework itself. Here are all the available defaults.

JWT_AUTH = {
    'JWT_ENCODE_HANDLER':
    'rest_framework_jwt.utils.jwt_encode_handler',

    'JWT_DECODE_HANDLER':
    'rest_framework_jwt.utils.jwt_decode_handler',

    'JWT_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_payload_handler',

    'JWT_PAYLOAD_GET_USER_ID_HANDLER':
    'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',

    'JWT_RESPONSE_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_response_payload_handler',

    'JWT_SECRET_KEY': settings.SECRET_KEY,
    'JWT_GET_USER_SECRET_KEY': None,
    'JWT_PUBLIC_KEY': None,
    'JWT_PRIVATE_KEY': None,
    'JWT_ALGORITHM': 'HS256',
    'JWT_VERIFY': True,
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LEEWAY': 0,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),  #设置过期时间
    'JWT_AUDIENCE': None,
    'JWT_ISSUER': None,

    'JWT_ALLOW_REFRESH': False,
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

    'JWT_AUTH_HEADER_PREFIX': 'JWT',
    'JWT_AUTH_COOKIE': None,

}

因为设置太多,具体可以参考JWT的官网设置,我们在项目添加两个重要一点的设置为:

import datetime

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),  # 设置过期时间
    'JWT_AUTH_HEADER_PREFIX': 'JWT',  #也可以设置Token,要和前端保持一致,我们使用默认就可以
}

5、云片网发送短信验证码

在这里,要实现手机注册的功能,需要用到第三方发短信的能力,使用云片网可以更好地帮助我们实现。注册登录上面会送0.5毛的短信,因此可以利用来验证我们的项目。

 

 

 

 注册的时候,签名需要加上名字后面test签名才能申请通过,必须这样以个人的名义申请,才能申请成功。

 

 apps/utils/yunpian.py

import json
import requests


class YunPian(object):
    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":"【李顺涛test】您的验证码是{code}。如非本人操作,请忽略本短信".format(code=code)
        }
        response = requests.post(self.single_send_url,data=params)
        re_dict = json.loads(response.text)
        print(re_dict)


if __name__ == '__main__':
    yunpian = YunPian("941ac85a42477492ea781615ca1fecf8")
    yunpian.send_sms("888888","156******17")

 

 

 

 测试成功,

 6、发送短信验证码接口

发送验证码的时候需要进行对手机号码的验证,因此前后端分离中用序列器来进行对数据的验证。

users.serializers.py:

import re
from datetime import datetime
from datetime import timedelta
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import VerifyCode

from MxShop.settings import REGEX_MOBILE

User = get_user_model()


class SmsSerializer(serializers.Serializer):
    mobile = serializers.CharField(max_length=11)

    def validate_mobile(self, mobile):
        """
        验证手机号码
        :param data:
        :return:
        """
        #手机是否注册
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError("用户已经存在")
        #验证手机是否合法
        if not re.match(REGEX_MOBILE,mobile):
            raise serializers.ValidationError("手机号码非法")
        #验证码发送频率
        one_minute_ago = datetime.now() - timedelta(hours=0,minutes=1,seconds=0)
        if VerifyCode.objects.filter(add_time__gt=one_minute_ago,mobile=mobile):
            raise serializers.ValidationError("距离上次发送没有到60s")
        return mobile

序列化器写好之后,在视图中编写对手机号码验证,以及数据库查询是否有重复,以及保存到数据库中的逻辑代码:

from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from random import choice

from utils.yunpian import YunPian
from MxShop.settings import APIKEY
from .serializers import SmsSerializer
from .models import VerifyCode


class SmsCodeViewset(CreateModelMixin,viewsets.GenericViewSet):
    """
    发送短信验证码
    """
    serializer_class = SmsSerializer
    def generate_code(self):
        """
        生成四位数字的验证码
        :return:
        """
        sends = "1234567890"
        random_str = []
        for i in range(4):
            random_str.append(choice(sends))

        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)
        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)

视图写好之后,配置注册验证码路由:

from users.views import SmsCodeViewset

router = DefaultRouter()

#注册url
router.register(r"codes",SmsCodeViewset,"codes")

 

 

 

 验证成功。

7、user serializer和validator验证

users.views.py

class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
    """
    用户
    """
    serializer_class = UserRegisterSerializer

users.serializers.py:

class UserRegisterSerializer(serializers.ModelSerializer):
    code = serializers.CharField(required=True, max_length=4, min_length=4,
                                 error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 }, help_text="验证码")
    #官网的validators验证
    username = serializers.CharField(required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(),message="用戶已經存在")])

    def validate_code(self, code):
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            last_records = verify_records[0]
            five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
            if five_minute_ago < last_records:
                raise serializers.ValidationError("验证码过期")
            if last_records.code != code:
                raise serializers.ValidationError("验证码错误")
        else:
            raise serializers.ValidationError("验证码错误")

    # 作用于所有的serilizers之上,attrs是validtae_data之后返回总的dict
    def validate(self, attrs):
        """
        将必填字段赋值,无用字段删除
        :param attrs:
        :return:
        """
        attrs["mobile"] = attrs["username"]
        del attrs["code"]
        return attrs

    class Meta:
        model = User
        fields = ("username", "code", "mobile")

MxShop.urls.py:

router.register(r"users",UserViewset,"users")

测试:

 

 8、Django信号量实现用户密码修改

users/serializers.py

class UserRegisterSerializer(serializers.ModelSerializer):
    code = serializers.CharField(required=True, write_only=True,max_length=4, min_length=4,label="验证码",
                                 error_messages={
                                     "blank": "请输入验证码",
                                     "required": "请输入验证码",
                                     "max_length": "验证码格式错误",
                                     "min_length": "验证码格式错误"
                                 }, help_text="验证码")
    #官网的validators验证
    username = serializers.CharField(label="用户名",required=True, allow_blank=False,
                                     validators=[UniqueValidator(queryset=User.objects.all(),message="用戶已經存在")])
    password = serializers.CharField(label="密码",write_only=True,
        style={
            "input_type":"password"
        }
    )
    # #密码是明文,重写保存密码为密文  Django的信号量机制可以修改密码(另一种方法)
    # def create(self, validated_data):
    #     user = super(UserRegisterSerializer, self).create(validated_data=validated_data)
    #     user.set_password(validated_data["password"])
    #     user.save()
    #     return user

    def validate_code(self, code):
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")
        if verify_records:
            last_records = verify_records[0]
            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("验证码错误")

    # 作用于所有的serilizers之上,attrs是validtae_data之后返回总的dict
    def validate(self, attrs):
        """
        将必填字段赋值,无用字段删除
        :param attrs:
        :return:
        """
        attrs["mobile"] = attrs["username"]
        del attrs["code"]
        return attrs

    class Meta:
        model = User
        fields = ("username", "code", "mobile","password")

上面注释掉的代码可以完成对密码的修改,保存到表中不是明文,而是密文。当然我们为了让代码的分离性更强,利用Django的信号量机制来进行对密码的密文保存,新建users/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()

@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        password = instance.password
        instance.set_password(password)
        instance.save()
        # Token.objects.create(user=instance)  用了JWT的方式,就不用Token

        #完成这个我们还要在apps中配置

users/apps.py:

from django.apps import AppConfig


class UsersConfig(AppConfig):
    name = 'users'
    verbose_name = "用户"

    def ready(self):
        import users.signals

完成配置之后运行users接口,post数据。返回如下:

 

 再到数据库中查看信息密码是密文了:

 

 9、vue和注册功能联调

  当注册成功的时候,有两种情况,一种是注册完成之后,自己拿着账号密码在登陆页面登录,另一种情况就是用户注册成功之后自动跳转到首页,并且已经登录,但这会出现情况,当用户注册并跳转手动登录,那么注册的时候,不会反回Token,当注册就自动登录的时候,那么需要后端在注册成功的时候,反回Token

 

   如果是注册成功之后,自动登陆的话,但是后端没有写JWT-Token的接口,因此我们需要到后端去写这个接口,将Token反回到前端。因此我们到users/views.py中的UserViewset中重载CreateModelMixincreate函数。

class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
    """
    用户
    """
    serializer_class = UserRegisterSerializer
    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)
        # token返回的时候,是返回serializer.data,因此要放在data里边
        re_dict = serializer.data
        payload = jwt_payload_handler(user)
        #要和前端保持一致,前端也叫token
        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)
  #重载上面create中的perform_create方法
  def perform_create(self, serializer): return serializer.save()

 

 

posted @ 2019-11-23 15:52  一知.半解  阅读(1031)  评论(0编辑  收藏  举报