Django+Vue开发生鲜电商平台之7.用户登录和注册功能

CuterCorley 等级 425 0 0

@[toc]

聪明是智慧者的天敌,傻瓜用嘴讲话,聪明的人用脑袋讲话,智慧的人用心讲话。所以永远记住,不要把自己当成最聪明的,最聪明的人相信总有别人比自己更聪明。 ——马云

Github和Gitee代码同步更新https://github.com/PythonWebProject/Django_Fresh_Ecommercehttps://gitee.com/Python_Web_Project/Django_Fresh_Ecommerce

一、DRF的token基本使用

1.DRF的token登录原理

基于DRF的前后端分离登录与单独使用Django登录的原理不同,不再需要CSRF验证,DRF提供了许多开箱即用的身份验证方案,并且还允许实现自定义方案。身份验证始终在视图的最开始处,在进行权限和限制检查之前以及在允许任何其他代码进行之前运行。 身份验证方案始终定义为类列表,DRF框架尝试对列表中的每个类进行身份验证,并使用成功进行身份验证的第一个类的返回值设置request.userrequest.auth

在使用前,需要在settings.py中进行配置:

# DRF配置
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ]
}

DRF提供4种认证类,包括BasicAuthentication、TokenAuthentication、SessionAuthentication和RemoteUserAuthentication: BasicAuthentication机制使用HTTP基本身份验证,该身份针对用户的用户名和密码进行了签名,在实际开发中一般仅适用于测试; TokenAuthentication身份验证方案使用基于令牌的简单HTTP身份验证方案,适用于客户端-服务器设置,例如本地台式机和移动客户端,适用于前后端分离项目,也是本项目中身份验证的重点; SessionAuthentication机制常见于浏览器,因为浏览器可以自动设置cookie,并将session和cookie传到浏览器,在后端分离项目中较少见; 对于RemoteUserAuthentication,通过此身份验证方案,可以将身份验证委派给Web服务器,要求服务器设置REMOTE_USER环境变量。

综上,选择TokenAuthentication,即选择Token的认证方式,需要在settings.py中添加到INSTALLED_APPS:

INSTALLED_APPS = [
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'apps.users.apps.UsersConfig',
    'goods.apps.GoodsConfig',
    'trade.apps.TradeConfig',
    'user_operation.apps.UserOperationConfig',
    'DjangoUeditor',
    'xadmin',
    'crispy_forms',
    'django.contrib.admin',
    'rest_framework',
    'django_filters',
    'corsheaders',
    'rest_framework.authtoken'
]

加入之后,执行makemigrationsmigrate命令进行数据映射,查看数据库可以看到生成新表authtoken_token,其表结构如下:

+---------+-------------+------+-----+---------+-------+ 
| Field   | Type        | Null | Key | Default | Extra | 
+---------+-------------+------+-----+---------+-------+ 
| key     | varchar(40) | NO   | PRI | NULL    |       | 
| created | datetime(6) | NO   |     | NULL    |       | 
| user_id | int(11)     | NO   | UNI | NULL    |       | 
+---------+-------------+------+-----+---------+-------+ 
3 rows in set (0.01 sec)                                 

其中,user_id是一个外键,指向users_userprofile表,表中的key(即token)和user之间具有一对一的关系。 但是在创建用户后并不会自动创建token,而是需要自己创建,可以使用HTTP请求模拟发送工具进行发送参数创建,使用Postman演示如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然,通过携带数据访问http://127.0.0.1:8000/api-token-auth/,生成了当前用户的token并获取到,在生成token的同时,自动将生成的token和当前用户存入表authtoken_token中,如下:

+------------------------------------------+----------------------------+---------+
| key                                      | created                    | user_id |
+------------------------------------------+----------------------------+---------+
| 236de0331b3e5a89665771f9aaff9be720cbba04 | 2020-07-27 08:29:47.306382 |       1 |
+------------------------------------------+----------------------------+---------+
1 row in set (0.01 sec)

现在已经获取到了token,就可以使用了。为了使客户端进行身份验证,令牌密钥应包含在Authorization HTTP标头中。密钥应以字符串文字Token作为前缀,并用空格分隔两个字符串。

此时再使用获取到的Token请求商品数据如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然,获取到了商品数据,可以体会到token比session的应用更方便,但是使用token验证也存在一些问题: 请求服务器生成的token只存在于一台被请求的服务器中,如果是分布式系统,为了数据一致,则需要将该服务器的数据同步到其他服务器,增加了操作和维护难度; token没有过期时间,显然这对于验证来说并不完善。

2.viewsets设置认证类

在使用token认证时,如果token不正确,则会抛出异常,并且如果对于本来不需要认证即可访问的公开数据要是再需要正确的token才能访问的话,就会降低项目的友好性,此时可以对token不采用全局设置,而在View中单独设置,settings.py如下:

# DRF配置
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ]
}

为了测试,在apps/goods/views.py中进行配置如下:

class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    '''商品列表页,并实现分页、搜索、过滤、排序'''

    queryset = Goods.objects.filter(is_delete=False)
    serializer_class = GoodsSerializer
    pagination_class = GoodsPagination
    authentication_classes = [TokenAuthentication]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filter_class = GoodsFilter
    search_fields = ['name', 'goods_brief', 'goods_desc']
    ordering_fields = ['sold_num', 'shop_price']

此时再请求http://127.0.0.1:8000/goods/,request.user即为当前用户admin。 当然在实际项目中由于goods是公开数据,因此不需要设置authentication_classes配置验证,还是为:

class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    '''商品列表页,并实现分页、搜索、过滤、排序'''

    queryset = Goods.objects.filter(is_delete=False)
    serializer_class = GoodsSerializer
    pagination_class = GoodsPagination
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filter_class = GoodsFilter
    search_fields = ['name', 'goods_brief', 'goods_desc']
    ordering_fields = ['sold_num', 'shop_price']

二、JSON Web Token登录

1.JWT原理

JSON Web Token (简称JWT),是目前最流行的跨域身份验证解决方案,使用基于Token的身份验证方法,在服务端不需要存储用户的登录记录。

在之前已经测试过,传统的前后端分离项目中,前端登录,后端生成对应的token信息并保存到session或数据库中。但是如果存在XSS漏洞,就可能存在cookie泄漏、信息不安全的问题。如果将验证信息保存到数据库中,会增加数据库的操作和存储开销;如果存到session中,又会增大服务器存储压力;如果采用加密算法来对用户信息加密得到token,则很容易被解密而泄漏用户信息。

JWT是一种开放的、行业标准的RFC7519方法,用于在双方之间安全地表示声明,JWT是凭据,使用加密算法加密,可以授予对资源的访问权限,具有简洁、自包含的特点。

JWT消息组成包含三部分:

  • Header头部 包含token类型和加密算法,并使用base64编码。
  • Payload负载 存放信息,包含用户id、签发者、面向的用户、接收方、签发时间和过期时间等,也通过base编码。
  • Signature签名 因为Header和Payload信息可以通过解码获取到具体信息并伪造信息进行请求,因此需要通过签名来进行识别,其使用Header中指定的算法对Header和Payload信息以及提供的密钥进行签名,来保证安全性。

相比于session,JWT将登录信息保存到本地,减轻了服务器的存储压力,并且可应用于单点登录

2.使用JWT完成用户认证

在DRF中使用JWT需要先安装依赖库,直接在虚拟环境中使用命令pip install djangorestframework-jwt安装即可。

安装后,需要在settings.py中进行配置:

# DRF配置
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ]
}

JSONWebTokenAuthentication可以对用户发送来的数据Token进行验证,并取出其中的user。

还需要在users.py中配置路由:

from django.conf.urls import url, include
from django.views.static import serve
from rest_framework.documentation import include_docs_urls
from rest_framework.routers import DefaultRouter
from rest_framework.authtoken import views
from rest_framework_jwt.views import obtain_jwt_token

import xadmin
from .settings import MEDIA_ROOT
from goods.views import GoodsListViewSet, CategoryViewSet

# Create a router and register our viewsets with it.

router = DefaultRouter()

# 配置goods的路由
router.register(r'goods', GoodsListViewSet, basename='goods')

# 配置categories的路由
router.register(r'categorys', CategoryViewSet, basename='categorys')

urlpatterns = [
       url(r'^xadmin/', xadmin.site.urls),
       url(r'^media/(?P<path>.*)$', serve, {'document_root':MEDIA_ROOT}),
       url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),

       # 商品列表页
       url(r'^', include(router.urls)),

       # 文档路由
       url(r'docs/', include_docs_urls(title='生鲜电商')),

       # DRF自带认证路由
       url(r'^api-token-auth/', views.obtain_auth_token, name='api_token_auth'),

       # JWT认证路由
       url(r'^jwt-auth/', obtain_jwt_token),
]

现对JWT进行获取和验证测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然获取到了JWT,并且可以正常作为用户信息进行登录访问。

3.Vue和JWT接口调试

在Vue中登录的接口为/login/,域名需要修改为local_host,如下:

//登录
export const login = params => {
  return axios.post(`${local_host}/login/`, params)
}

定义登录的Vue组件为src/views/login/login.vue,如下:

methods:{
    login(){
    var that = this;
    login({
        username:this.userName, //当前页码
        password:this.parseWord
    }).then((response)=> {
        console.log(response);
        //本地存储用户信息
        cookie.setCookie('name',this.userName,7);
        cookie.setCookie('token',response.data.token,7)
        //存储在store
        // 更新store数据
        that.$store.dispatch('setInfo');
        //跳转到首页页面
        this.$router.push({ name: 'index'})
        })
        .catch(function (error) {
        if("non_field_errors" in error){
            that.error = error.non_field_errors[0];
        }
        if("username" in error){
            that.userNameError = error.username[0];
        }
        if("password" in error){
            that.parseWordError = error.password[0];
        }
        });

    },
    errorUnshow(){
    this.error = false;
    }
},

在获取到username和password之后,即可保存到cookie中,并设置有效期为7天。通过setInfo更新store数据,再根据src/store/actions.js中export const setInfo = makeAction(types.SET_INFO);找到 src/store/mutations.js,如下:

[types.SET_INFO] (state) {
    state.userInfo = {
        name:cookie.getCookie('name'),
        token:cookie.getCookie('token')
    }
    console.log(state.userInfo);
},

用于将登录信息保存到状态中,进行测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

可以看到,在登录之前,state中name和token均为空,登录之后即变为当前用户的用户名和JWT。

在用户进行登录提交后,通过对用户名和密码进行比对,但是如果通过手机号码登录,就可能失败,因为登录时obtain_jwt_token查询数据库默认查询的是用户名和密码,而未查询手机号码,因此需要自定义用户认证方法,settings.py中配置如下:

# DRF配置
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ]
}

# 自定义用户认证配置
AUTHENTICATION_BACKENDS = [
    'users.views.CustomBackend',
]

apps/users/views.py中定义自定义验证类如下:

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


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) and user.is_delete != True:
                return user
        except Exception as e:
            return None

urls.py中配置路由如下:

from django.conf.urls import url, include
from django.views.static import serve
from rest_framework.documentation import include_docs_urls
from rest_framework.routers import DefaultRouter
from rest_framework.authtoken import views
from rest_framework_jwt.views import obtain_jwt_token

import xadmin
from .settings import MEDIA_ROOT
from goods.views import GoodsListViewSet, CategoryViewSet

# Create a router and register our viewsets with it.

router = DefaultRouter()

# 配置goods的路由
router.register(r'goods', GoodsListViewSet, basename='goods')

# 配置categories的路由
router.register(r'categorys', CategoryViewSet, basename='categorys')

urlpatterns = [
       url(r'^xadmin/', xadmin.site.urls),
       url(r'^media/(?P<path>.*)$', serve, {'document_root':MEDIA_ROOT}),
       # url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),

       # 商品列表页
       url(r'^', include(router.urls)),

       # 文档路由
       url(r'docs/', include_docs_urls(title='生鲜电商')),

       # DRF自带认证路由
       url(r'^api-token-auth/', views.obtain_auth_token, name='api_token_auth'),

       # JWT认证路由
       url(r'^login/', obtain_jwt_token),
]

显示: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然,获取到了token并成功进行了验证。

JWT还有很多设置,包括过期时间等,可以根据需要进行配置,如下:

# JWT配置
JWT_AUTH = {
    # 过期时间
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
    # 请求头前缀
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

三、用户注册功能实现

1.云片网发送短信验证码

在注册页面输入手机号发送验证码,后端需要有相应的接口来发送验证码,在成功和失败后需要进行相应的操作。 发送短信验证码需要使用第三方服务,可以使用云片网、阿里妈妈等平台的短信验证码服务,这里选择云片网。

在使用之前需要新增签名和模板,具体操作可参考https://blog.csdn.net/CUFEECR/article/details/106941804

其中用于发送验证码的单条发送接口文档为https://www.yunpian.com/official/document/sms/zh_cn/domestic_single_send,可以看到接口为https://sms.yunpian.com/v2/sms/single_send.json,请求参数中必传参数为apikey、mobile和text。

在apps下新建一个Python Package为utils作为工具目录,下新建yunpian.py用于短信发送测试如下:

import requests
import json

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': '【Python进化讲堂】欢迎您注册Fresh_Ecommerce ,验证码:{}(5分钟内有效,如非本人操作,请忽略)'.format(code)
        }
        response = requests.post(self.single_send_url, data=params)
        re_dict = json.loads(response.text)
        print(re_dict)


if __name__ == '__main__':
    yunpian = YunPian('edf71361381f31b3957beda37f20xxxx')  # 改为你自己的apikey
    yunpian.send_sms('1234', '13312345678')  # 改为你自己的手机号

运行该文件,打印:

{'code': 0, 'msg': '发送成功', 'count': 1, 'fee': 0.05, 'unit': 'RMB', 'mobile': '13312345678', 'sid': 56592475448}

则发送短信成功。

除此之外,还可以使用云片的Python SDK进行短信发送,可参考http://oss-standard.oss-cn-hangzhou.aliyuncs.com/yunpian/app/apiweb/pythonSDK.mp4https://github.com/yunpian/yunpian-python-sdk进行使用。

2.DRF实现发送短信验证码接口

需要在DRF中接入短信验证码发送。 在发送短信验证码前需要进行验证,包括手机号是否合法、是否被注册过和注册频率等,在serializer中进行验证,apps/users下新建serializers.py如下:

import re
from datetime import datetime, timedelta

from rest_framework import serializers
from django.contrib.auth import get_user_model

from Fresh_Ecommerce.settings import REGEX_MOBILE
from .models import VerifyCode

User = get_user_model()

class SmsSerializer(serializers.Serializer):
    '''短信发送序列化'''

    mobile = serializers.CharField(max_length=11)

    def validate_mobile(self, mobile):
        '''验证手机号码'''


        # 验证手机号码是否合法
        if not re.match(REGEX_MOBILE, mobile):
            raise serializers.ValidationError('手机号格式有误,请重新输入')

        # 验证手机是否注册
        if User.objects.filter(mobile=mobile).count():
            raise serializers.ValidationError('手机号已经被注册过,请更换手机号重新注册或直接使用该手机号登录')

        # 验证短信发送频率
        one_minute_ago = datetime.now() - timedelta(minutes=1)
        if VerifyCode.objects.filter(add_time__gt=one_minute_ago, mobile=mobile).count():
            raise serializers.ValidationError('验证码发送频率过快,请稍后再试')

        return mobile

apps/users//views.py下创建发送短信的视图如下:

class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet):
    '''发送短信验证码'''

    serializer_class = SmsSerializer

    def generate_code(self):
        '''生成4位数验证码'''
        seeds = '1234567890'
        random_str = []
        for i in range(4):
            random_str.append(choice(seeds))

        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']
        code = self.generate_code()
        sms_status = yunpian.send_sms(code, 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,
                'code': code
            }, status=status.HTTP_201_CREATED)

在定义发送短信验证码的View时,create()方法中调用serializer.is_valid()时需要加入参数raise_exception=True,这样在执行时如果is_valid()方法出错就会抛出异常,不会再向下执行,并且由DRF捕捉返回400状态码,便于在前端查看。

apps/utils/yunpian.py修改如下:

import requests
import json

from Fresh_Ecommerce.settings import APIKEY

class YunPian(object):
    def __init__(self):
        self.api_key = APIKEY
        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': '【Python进化讲堂】欢迎您注册Fresh_Ecommerce ,验证码:{}(5分钟内有效,如非本人操作,请忽略)'.format(code)
        }
        response = requests.post(self.single_send_url, data=params)
        re_dict = json.loads(response.text)
        return re_dict


yunpian = YunPian()


if __name__ == '__main__':
    yunpian.send_sms('1234', '13312345678')  # 改为你自己的手机号

settings.py配置如下:

# 手机号码验证正则表达式
REGEX_MOBILE = '^1[35789]\d{9}$|^147\d{8}$'

# 云片网APIKEY
APIKEY = 'edf71361381f31b3957beda37f20xxxx'

urls.py中配置路由如下:

# 配置短信验证码路由
router.register(r'codes', SmsCodeViewSet, basename='codes')

进行请求测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然,已经可以对手机号进行验证,并且发送成功后会返回相应信息。

说明: 因为接口请求需要用POST方法,因此开始直接使用GET方法会失败,DRF提供了在页面直接用POST方法发送数据的功能,这对以后的测试提供了极大的方便。

此时查看数据库,可以看到刚刚保存的验证码如下:

+----+------+-------------+----------------------------+-----------+
| id | code | mobile      | add_time                   | is_delete |
+----+------+-------------+----------------------------+-----------+
|  1 | 4745 | 13311111111 | 2020-07-28 17:10:38.142213 |         0 |
+----+------+-------------+----------------------------+-----------+
1 row in set (0.01 sec)

3.用户序列化和验证器

注册页面需要传递3个数据,即手机号码、验证码和密码,对应3个字段,需要定义视图并验证。

serializers.py中定义用户注册的序列化如下:

class UserRegSerializer(serializers.ModelSerializer):
    '''用户序列化'''
    code = serializers.CharField(max_length=4, min_length=4,
                                 help_text='验证码',
                                 error_messages={
                                     'required': '请输入验证码',
                                     'blank': '请输入验证码',
                                     'max_length': '请输入4位验证码',
                                     'min_length': '请输入4位验证码'
                                 })
    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_record = verify_records[0]
            five_minute_ago = datetime.now() - timedelta(minutes=5)
            # 验证验证码是否过期
            if five_minute_ago > last_record.add_time:
                raise serializers.ValidationError('验证码已过期,请重新验证')
            # 验证验证码是否正确
            if last_record.code != code:
                raise serializers.ValidationError('验证码错误')
        else:
            raise serializers.ValidationError('数据有误,请重新验证')

    def validate(self, attrs):
        attrs['mobile'] = attrs['username']
        del attrs['code']
        return attrs

    class Meta:
        model = User
        fields = ['username', 'code', 'mobile']

因为code字段只是为了验证临时生成的、并不需要保存到用户数据表中,因此在验证之后需要删除,在validate(attrs)方法中实现即可,同时因为人为设定前端传递回来的手机号数据变量名为username而非mobile,因此需要在validate(attrs)方法中为attrs变量增加键为mobile的数据,并且要修改UserProfile模型的mobile字段允许为空,修改如下:

class UserProfile(AbstractUser):
    '''用户'''
    name = models.CharField(max_length=30, null=True, blank=True, verbose_name='姓名')
    birthday = models.DateField(null=True, blank=True, verbose_name='出生日期')
    gender = models.CharField(max_length=6, choices=(('male', u'男'), ('female', u'女')), default='female',
                              verbose_name='性别')
    mobile = models.CharField(max_length=11, null=True, blank=True, verbose_name='电话')
    email = models.CharField(max_length=50, null=True, blank=True, verbose_name='邮箱')

    is_delete = models.BooleanField(default=False, verbose_name='是否删除')

    class Meta:
        verbose_name = '用户'
        verbose_name_plural = '用户'

    def __str__(self):
        return self.username

修改后需要将变化映射到数据库中。

对于字段的验证,除了默认的required、max_length、min_length等验证方式,DRF还提供了专业的验证器,包括UniqueValidator、UniqueTogetherValidator、UniqueForDateValidator和Advanced field defaults等。

views.py定义视图如下:

class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
    '''用户'''

    serializer_class = UserRegSerializer

urls.py中注册路由如下:

# 配置注册路由
router.register(r'users', UserViewSet, basename='users')

进行访问测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然,对于多个字段的验证,如果某一个字段验证失败,则提示该字段的错误信息,如果多个字段验证失败,则将这些字段的错误信息都显示出来。

从之前的DRF的测试中可以总结出,DRF请求消息返回的规范为:

http_code
{
    field1: ['', ''],
    field2: [],
    ...
    'non_fields_error'
}

即包含HTTP状态码和具体信息,如果是返回的错误信息可以用于对前端的有误区域进行标亮显示,以便于用户重新输入。

4.密码设置的多种方式

进一步完善序列化如下:

class UserRegSerializer(serializers.ModelSerializer):
    '''用户序列化'''
    code = serializers.CharField(max_length=4, min_length=4, label='验证码',
                                 help_text='验证码',
                                 error_messages={
                                     'required': '请输入验证码',
                                     'blank': '请输入验证码',
                                     'max_length': '请输入4位验证码',
                                     'min_length': '请输入4位验证码'
                                 })
    username = serializers.CharField(required=True, allow_blank=False,  label='用户名', validators=[UniqueValidator(queryset=User.objects.filter(is_delete=False), message='用户已经存在')])
    password = serializers.CharField(label='密码', style={'input_type': 'password'})

    def validate_code(self, code):
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time')

        # 验证验证码是否存在
        if verify_records:
            last_record = verify_records[0]
            five_minute_ago = datetime.now() - timedelta(minutes=5)
            # 验证验证码是否过期
            if five_minute_ago > last_record.add_time:
                raise serializers.ValidationError('验证码已过期,请重新验证')
            # 验证验证码是否正确
            if last_record.code != code:
                raise serializers.ValidationError('验证码错误')
        else:
            raise serializers.ValidationError('数据有误,请重新验证')

    def validate(self, attrs):
        attrs['mobile'] = attrs['username']
        del attrs['code']
        return attrs

    class Meta:
        model = User
        fields = ['username', 'code', 'mobile', 'password']

此时再访问模拟注册如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能 显然,报错如下:

raise type(exc)(msg)
AttributeError: Got AttributeError when attempting to get a value for field `code` on serializer `UserRegSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `UserProfile` instance.
Original exception text was: 'UserProfile' object has no attribute 'code'.

报错提示很明显,UserProfile没有code属性。具体来说,这是因为Meta中指定了fields = ['username', 'code', 'mobile', 'password'],包含code字段,而在验证时为了判断验证码的正误而临时加入code字段,但是在validate(attrs)又将其删去,导致在序列化时找不到code字段,因此出错,这是需要将字段的write_only设置True,以确保在更新或创建实例时可以使用该字段,但是在序列化表示形式时不包括该字段。

同时查询用户表如下:

+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
| id | password                                                                       | last_login                 | is_superuser | username    | first_name | last_name | is_staff | is_active | date_joined                | name | birthday | gender | mobile      | email       | is_delete |
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
|  1 | pbkdf2_sha256$180000$wpfCm77Dcpee$rHfFjBNZ2SzLLHdd0ZtbiIRqNB86VvgwTJv6ZCXTbfk= | 2020-07-28 20:11:10.453289 |            1 | admin       |            |           |        1 |         1 | 2020-07-20 10:12:43.787964 | NULL | NULL     | female |             | 123@123.com |         0 |
|  2 | pbkdf2_sha256$180000$VqEN1rdsS4ts$hgqzLLzxvIk3au1osUB/yrJA5ffFubE87gRBumUAqUE= | NULL                       |            1 | admin2      |            |           |        1 |         1 | 2020-07-27 18:46:54.826360 | NULL | NULL     | female |             | 456@123.com |         0 |
|  4 | admin12345                                                                     | NULL                       |            0 | 13388888888 |            |           |        0 |         1 | 2020-07-28 20:24:01.808609 | NULL | NULL     | female | 13388888888 | NULL        |         0 |
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
3 rows in set (0.01 sec)

显然,刚刚的用户已经保存到用户表中,但是密码为明文,存在很大的风险,需要进行加密设置,可以重载create(validated_data)实现密码设置即可。除此之外,为了password字段不返回前端,也需要为其加write_only属性,serializers.py完善如下:

class UserRegSerializer(serializers.ModelSerializer):
    '''用户序列化'''
    code = serializers.CharField(max_length=4, min_length=4, label='验证码', write_only=True,
                                 help_text='验证码',
                                 error_messages={
                                     'required': '请输入验证码',
                                     'blank': '请输入验证码',
                                     'max_length': '请输入4位验证码',
                                     'min_length': '请输入4位验证码'
                                 })
    username = serializers.CharField(required=True, allow_blank=False,  label='用户名', validators=[UniqueValidator(queryset=User.objects.filter(is_delete=False), message='用户已经存在')])
    password = serializers.CharField(label='密码',  write_only=True, style={'input_type': 'password'})

    def create(self, validated_data):
        user = super(UserRegSerializer, 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_record = verify_records[0]
            five_minute_ago = datetime.now() - timedelta(minutes=5)
            # 验证验证码是否过期
            if five_minute_ago > last_record.add_time:
                raise serializers.ValidationError('验证码已过期,请重新验证')
            # 验证验证码是否正确
            if last_record.code != code:
                raise serializers.ValidationError('验证码错误')
        else:
            raise serializers.ValidationError('数据有误,请重新验证')

    def validate(self, attrs):
        attrs['mobile'] = attrs['username']
        del attrs['code']
        return attrs

    class Meta:
        model = User
        fields = ['username', 'code', 'mobile', 'password']

此时再进行测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然,测试成功,在提交之后返回数据,查询用户表如下:

+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
| id | password                                                                       | last_login                 | is_superuser | username    | first_name | last_name | is_staff | is_active | date_joined                | name | birthday | gender | mobile      | email       | is_delete |
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
|  1 | pbkdf2_sha256$180000$wpfCm77Dcpee$rHfFjBNZ2SzLLHdd0ZtbiIRqNB86VvgwTJv6ZCXTbfk= | 2020-07-28 20:11:10.453289 |            1 | admin       |            |           |        1 |         1 | 2020-07-20 10:12:43.787964 | NULL | NULL     | female |             | 123@123.com |         0 |
|  2 | pbkdf2_sha256$180000$VqEN1rdsS4ts$hgqzLLzxvIk3au1osUB/yrJA5ffFubE87gRBumUAqUE= | NULL                       |            1 | admin2      |            |           |        1 |         1 | 2020-07-27 18:46:54.826360 | NULL | NULL     | female |             | 456@123.com |         0 |
|  4 | admin12345                                                                     | NULL                       |            0 | 13388888888 |            |           |        0 |         1 | 2020-07-28 20:24:01.808609 | NULL | NULL     | female | 13388888888 | NULL        |         0 |
|  5 | pbkdf2_sha256$180000$dKdR8lvqcymO$OZunKajLJo6q+b3ub+NYNTuKNyOzlz9wGN08DYobUrY= | NULL                       |            0 | 13377777777 |            |           |        0 |         1 | 2020-07-28 20:55:39.193938 | NULL | NULL     | female | 13377777777 | NULL        |         0 |
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
4 rows in set (0.00 sec)

显然,新注册的用户密码位密文,而不再是明文。 除了用以上方式实现密码设置,还可以通过Django信号量实现,具体可查看https://docs.djangoproject.com/en/1.10/ref/signals/。其中一类信号是模型信号,django.db.models.signals模块定义了模型系统发送的一组信号,对模型进行操作后,Django会发出全局信号,捕捉到之后可以加入需要的业务逻辑,具体包括pre_initpost_initpre_savepost_save等,这里我们使用post_save信号实现密码设置。

在apps/users下创建signals.py如下:

from django.db.models.signals import post_save
from django.contrib.auth import get_user_model
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

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

在apps/users/apps.py中进行配置如下:

from django.apps import AppConfig

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

    def ready(self):
        import users.signals

serializers.py去掉设置密码的逻辑如下:

class UserRegSerializer(serializers.ModelSerializer):
    '''用户序列化'''
    code = serializers.CharField(max_length=4, min_length=4, label='验证码', write_only=True,
                                 help_text='验证码',
                                 error_messages={
                                     'required': '请输入验证码',
                                     'blank': '请输入验证码',
                                     'max_length': '请输入4位验证码',
                                     'min_length': '请输入4位验证码'
                                 })
    username = serializers.CharField(required=True, allow_blank=False,  label='用户名', validators=[UniqueValidator(queryset=User.objects.filter(is_delete=False), message='用户已经存在')])
    password = serializers.CharField(label='密码',  write_only=True, style={'input_type': 'password'})

    def validate_code(self, code):
        verify_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time')

        # 验证验证码是否存在
        if verify_records:
            last_record = verify_records[0]
            five_minute_ago = datetime.now() - timedelta(minutes=5)
            # 验证验证码是否过期
            if five_minute_ago > last_record.add_time:
                raise serializers.ValidationError('验证码已过期,请重新验证')
            # 验证验证码是否正确
            if last_record.code != code:
                raise serializers.ValidationError('验证码错误')
        else:
            raise serializers.ValidationError('数据有误,请重新验证')

    def validate(self, attrs):
        attrs['mobile'] = attrs['username']
        del attrs['code']
        return attrs

    class Meta:
        model = User
        fields = ['username', 'code', 'mobile', 'password']

此时在后台进行测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

前台测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

再查询数据库,如下:

+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
| id | password                                                                       | last_login                 | is_superuser | username    | first_name | last_name | is_staff | is_active | date_joined                | name | birthday | gender | mobile      | email       | is_delete |
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
|  1 | pbkdf2_sha256$180000$wpfCm77Dcpee$rHfFjBNZ2SzLLHdd0ZtbiIRqNB86VvgwTJv6ZCXTbfk= | 2020-07-28 20:11:10.453289 |            1 | admin       |            |           |        1 |         1 | 2020-07-20 10:12:43.787964 | NULL | NULL     | female |             | 123@123.com |         0 |
|  2 | pbkdf2_sha256$180000$VqEN1rdsS4ts$hgqzLLzxvIk3au1osUB/yrJA5ffFubE87gRBumUAqUE= | NULL                       |            1 | admin2      |            |           |        1 |         1 | 2020-07-27 18:46:54.826360 | NULL | NULL     | female |             | 456@123.com |         0 |
|  4 | admin12345                                                                     | NULL                       |            0 | 13388888888 |            |           |        0 |         1 | 2020-07-28 20:24:01.808609 | NULL | NULL     | female | 13388888888 | NULL        |         0 |
|  5 | pbkdf2_sha256$180000$dKdR8lvqcymO$OZunKajLJo6q+b3ub+NYNTuKNyOzlz9wGN08DYobUrY= | NULL                       |            0 | 13377777777 |            |           |        0 |         1 | 2020-07-28 20:55:39.193938 | NULL | NULL     | female | 13377777777 | NULL        |         0 |
|  6 | pbkdf2_sha256$180000$h6Daqay8tHp3$7Cuw+iigsrqBFldUJybFt8hq5SDwjiwxXhgRMYvs6iw= | NULL                       |            0 | 13366666666 |            |           |        0 |         1 | 2020-07-28 21:28:27.957466 | NULL | NULL     | female | NULL        |             |         0 |
|  7 | pbkdf2_sha256$180000$JM8j2c0fl81i$DesmherPz0ZUC+orr5kDREVmDJPTc4ahb4vL3Zd/s5s= | NULL                       |            0 | 13355555555 |            |           |        0 |         1 | 2020-07-28 21:31:56.062071 | NULL | NULL     | female | 13355555555 | NULL        |         0 |
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
6 rows in set (0.00 sec)

显然,用户均创建成功,并且密码为密文,说明信号成功实现了密码设置。

四、Vue实现注册功能

现在实现前端注册功能,前端src/views/register下定义了注册的组件register.vue,如下:

isRegister(){
    var that = this;
    register({
        password:that.password,
        username:that.mobile ,
        code:that.code,
    }).then((response)=> {
      cookie.setCookie('name',response.data.username,7);
      cookie.setCookie('token',response.data.token,7)
      //存储在store
      // 更新store数据
      that.$store.dispatch('setInfo');
      //跳转到首页页面
      this.$router.push({ name: 'index'})

  })
  .catch(function (error) {
    that.error.mobile = error.username?error.username[0]:'';
    that.error.password = error.password?error.password[0]:'';
    that.error.username = error.mobile?error.mobile[0]:'';
    that.error.code = error.code?error.code[0]:'';
  });
},

因为一般在注册成功之后会有两种情况: 一种是注册成功后直接自动登录并跳转到指定页,这里采用的就是这种方式; 另一种是注册后不自动登录,但是跳转到登录页或其他页面,需要自己手动登录,这时只需要注释掉

cookie.setCookie('name',response.data.username,7);
cookie.setCookie('token',response.data.token,7)
//存储在store
// 更新store数据
that.$store.dispatch('setInfo');

部分即可。 这里传递了注册需要用到的3个字段,并且使用了register接口,在api.js中定义修改如下:

//注册

export const register = parmas => { return axios.post(`${local_host}/users/`, parmas) }

在实现注册后自动登录的效果时,还需要设置token,但是后端还并未设置token接口,需要进行配置,views.py配置如下:

class UserViewSet(CreateModelMixin, viewsets.GenericViewSet):
    '''用户'''

    serializer_class = UserRegSerializer
    queryset = User.objects.filter(is_delete=False)

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)
        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)

    def perform_create(self, serializer):
         return serializer.save()

进行测试如下: Django+Vue开发生鲜电商平台之7.用户登录和注册功能

显然,已经可以正常注册并登录。 可以看到,在登录之后可以退出,在src/views/head/head.vue中实现逻辑如下:

<a @click="loginOut">退出</a>
...
loginOut(){
    // this.$http.get('/getMenu')
    //     .then((response)=> {

            //跳转到登录
            this.$router.push({ name: 'login' })
    //     })
    //     .catch(function (error) {
    //       console.log(error);
    // });
},

src/views/head/shophead.vue如下:

<a @click="loginOut">退出</a>
...
loginOut(){
        cookie.delCookie('token');
        cookie.delCookie('name');
        //重新触发store
        //更新store数据
        this.$store.dispatch('setInfo');
        //跳转到登录
        this.$router.push({name: 'login'})
      },

显然,退出登录的逻辑是cookie中删除token和name,并重定向到登录页。

本文原文首发来自博客专栏Python Web开发实战,由本人转发至https://www.helloworld.net/p/Mndaigaiw7Ij8,其他平台均属侵权,可点击https://blog.csdn.net/CUFEECR/article/details/107652584查看原文,也可点击https://blog.csdn.net/CUFEECR浏览更多优质原创内容。

收藏
评论区

相关推荐

【Golang】Golang + jwt 实现简易用户认证
<p本文已同步发布到我的个人博客:<a href"https://links.jianshu.com/go?tohttps%3A%2F%2Fglorin.xyz%2F2019%2F11%2F23%2FGolangjwtsimpleauth%2F" target"_blank"https://glorin.xyz/2019/11/23/Golang
Django+Vue开发生鲜电商平台之1.项目介绍
我永远相信只要永不放弃,我们还是有机会的。最后,我们还是坚信一点,这世界上只要有梦想,只要不断努力,只要不断学习,不管你长得如何,不管是这样,还是那样,男人的长相往往和他的的才华成反比。今天很残酷,明天更残酷,后天很美好,但绝对大部分是死在明天晚上,所以每个人不要放弃今天。 马云 本项目旨在使用Django、Vue和REST Framewor
uni-app使用uniCloud时做类似于拦截器和请求结果再处理(类似于请求和响应拦截)
想要在使用uniCloud的使用拦截请求怎么办 再次封装uniCloud.callFunction特别说明 这里的token是我自己存储成token 如果你使用了uniid官方的推荐是 ('uniidtoken') ('uniidtokenexpired') 存储了uniidtoken后请求会自动携带 这里的res.result.code0是因为我的云
记录Vue项目实现axios请求头带上token
在vue项目中首先 npm 命令安装 axios: npm install axios Saxios 的封装使用请求带上token,token通过登录获取存在vuex,为防止刷新丢失token使用持久化 vuexpersistedstate 插件保存数据npm i S vuexpersistedstateimport persistedStat
JWT 认证方案学习
定义JWT (JSON WEB TOKEN) 是一种安全通讯标准,它定义了一种紧凑自包含的方式,用于在各方之间安全的传输 JSON 对象。常见应用场景是API之间的认证通讯。 一般的用户认证流程 1、请求方发送账户密码到服务器,验证账户可用性。 2、验证成功,生成 session,保存在服务端。 3、服务端返回一个 s
云函数手撸用户体系
使用云函数实现用户系统数据库为腾讯云TDSQL其它服务商云函数 通用 只需修改index.js返回参数即可主要有用户注册 用户登陆 邮箱发送验证码 邮箱验证码校检 邮箱绑定 邮箱解绑 邮箱验证码登陆 生成token 校验token 其它功能可以在此基础上拓展纯手撸代码 云函数环境为nodejs12.13由于我比较穷 就不带大家使用短信服务了
Django+Vue开发生鲜电商平台之1.项目介绍
我永远相信只要永不放弃,我们还是有机会的。最后,我们还是坚信一点,这世界上只要有梦想,只要不断努力,只要不断学习,不管你长得如何,不管是这样,还是那样,男人的长相往往和他的的才华成反比。今天很残酷,明天更残酷,后天很美好,但绝对大部分是死在明天晚上,所以每个人不要放弃今天。 马云本项目旨在使用Django、Vue和REST Framewor
Django+Vue开发生鲜电商平台之3.数据模型设计和资源导入
永远不要跟别人比幸运,我从来没想过我比别人幸运,我也许比他们更有毅力,在最困难的时候,他们熬不住了,我可以多熬一秒钟、两秒钟。 ——马云Github和Gitee代码同步更新:;。在正式开发项目之前,要确定数据库和表结构。 一、项目初始化在虚拟环境安装好之后,需要安装Django和Django REST framework,直接
Django+Vue开发生鲜电商平台之4.Restful API和Vue介绍
也许今天你是最好的,但未必明天还最好;今天也许你是最差的,但社会给了你很多的机会,只要你把握,只要努力,总会有机会。 ——马云Github和Gitee代码同步更新:;。后端架构搭建好之后,需要搭建前端架构。 一、Restful API介绍 1.前后端分离优缺点近年来,随着多种平台类型(PC端、Android端、Mac端、iPhone端、P
Django+Vue开发生鲜电商平台之5.使用DRF实现商品列表页和过滤
胸怀是非常重要的,一个人有眼光没胸怀是很倒霉的。三国演义的周瑜就是眼光很厉害,胸怀很小,所以被诸葛亮气死了。宰相肚里面能撑船,说明宰相怨气太多了。他不可能每天跟人解释,只能干,用胸怀跟人解释。每个人的胸怀是靠委屈撑大的。 ——马云Github和Gitee代码同步更新:;。 一、普通方式实现商品列表页先了解Django中实现Json数据传递的基
Django+Vue开发生鲜电商平台之6.使用Vue实现商品类别和商品数据前台显示
什么是胸怀?胸怀是人生的志向和抱负,胸怀是人格的品位和质量,胸怀是人对待世界万物气量和风度的定位。胸怀,能使弱者走过别人不敢走的路,攀上别人难以达到的高峰;胸怀,可以使先天低矮的人在别人眼里变得挺拔高大;胸怀,能使一名柔弱的女子充满大丈夫的英雄气概;胸怀,也使一个弱质变得体格健壮。 ——马云Github和Gitee代码同步更新:;。现在将DRF
Django+Vue开发生鲜电商平台之7.用户登录和注册功能
@toc 聪明是智慧者的天敌,傻瓜用嘴讲话,聪明的人用脑袋讲话,智慧的人用心讲话。所以永远记住,不要把自己当成最聪明的,最聪明的人相信总有别人比自己更聪明。 ——马云Github和Gitee代码同步更新:;。 一、DRF的token基本使用 1.DRF的token登录原理基于DRF的前后端分离登录与单独使用Django登录的原理不同,
Django+Vue开发生鲜电商平台之8.商品详情页功能实现
不走康庄大道,我自己喜欢做什么要比别人怎么看我更重要。 ——李彦宏Github和Gitee代码同步更新:;。 一、viewsets实现商品详情页商品详情页效果如下:可以看到,左侧有商品轮播图,右侧是商品的详情信息,包括商品名称、商品描述、是否包邮、市场价、本店价、销量、库存量、购物车按钮、收藏按钮,还包括富文本详情和热卖商品等。apps/go
Django+Vue开发生鲜电商平台之11.首页、商品数量、缓存和限速功能开发
青,取之于蓝而青于蓝;冰,水为之而寒于水。 ——《荀子·劝学》Github和Gitee代码同步更新:;。 一、首页功能完善首页待完善的功能包括轮播图、新品尝鲜、系列商品等。 1.轮播图实现轮播图包括3张图片,链接对应3个商品,先在apps/goods/serializers.py中定义序列化如下:pythonclass Banner
go每日一库 [go-rate] 速率限制器
关于我gorate是速率限制器库,基于 Token Bucket(令牌桶)算法实现。 gorate被用在生产中 用于遵守GitHub API速率限制。速率限制可以完成一些特殊的功能需求,包括但不限于服务器端垃圾邮件保护、防止api调用饱和等。 库使用说明 构造限流器我们首先构造一个限流器对象:golimiter : NewLimi