@[toc]
聪明是智慧者的天敌,傻瓜用嘴讲话,聪明的人用脑袋讲话,智慧的人用心讲话。所以永远记住,不要把自己当成最聪明的,最聪明的人相信总有别人比自己更聪明。 ——马云
Github和Gitee代码同步更新: https://github.com/PythonWebProject/Django_Fresh_Ecommerce; https://gitee.com/Python_Web_Project/Django_Fresh_Ecommerce。
一、DRF的token基本使用
1.DRF的token登录原理
基于DRF的前后端分离登录与单独使用Django登录的原理不同,不再需要CSRF验证,DRF提供了许多开箱即用的身份验证方案,并且还允许实现自定义方案。身份验证始终在视图的最开始处,在进行权限和限制检查之前以及在允许任何其他代码进行之前运行。 身份验证方案始终定义为类列表,DRF框架尝试对列表中的每个类进行身份验证,并使用成功进行身份验证的第一个类的返回值设置request.user和request.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'
]
加入之后,执行makemigrations
和migrate
命令进行数据映射,查看数据库可以看到生成新表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演示如下:
显然,通过携带数据访问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请求商品数据如下:
显然,获取到了商品数据,可以体会到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进行获取和验证测试如下:
显然获取到了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);
},
用于将登录信息保存到状态中,进行测试如下:
可以看到,在登录之前,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),
]
显示:
显然,获取到了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.mp4和https://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')
进行请求测试如下:
显然,已经可以对手机号进行验证,并且发送成功后会返回相应信息。
说明: 因为接口请求需要用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')
进行访问测试如下:
显然,对于多个字段的验证,如果某一个字段验证失败,则提示该字段的错误信息,如果多个字段验证失败,则将这些字段的错误信息都显示出来。
从之前的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']
此时再访问模拟注册如下: 显然,报错如下:
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']
此时再进行测试如下:
显然,测试成功,在提交之后返回数据,查询用户表如下:
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
| 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_init
、post_init
、pre_save
和post_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']
此时在后台进行测试如下:
前台测试如下:
再查询数据库,如下:
+----+--------------------------------------------------------------------------------+----------------------------+--------------+-------------+------------+-----------+----------+-----------+----------------------------+------+----------+--------+-------------+-------------+-----------+
| 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()
进行测试如下:
显然,已经可以正常注册并登录。 可以看到,在登录之后可以退出,在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浏览更多优质原创内容。