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状态码和具体信息,如果是返回的错误信息可以用于对前端的有误区域进行标亮显示,以便于用户重新输入。