一、用户登录和手机注册
1、DRF的token登录和原理
在前后端不分离中,登录需要添加csrf_token,要进行安全验证,但却在前后端分离中,我们不需要进行csrf_token验证,为什么不用验证呢?因为前端是用APP,安卓来写的,因此这一定是跨站验证,因此不用csrf_token验证,但DRF官方文档给了验证用户方式。
因此将这个配置到项目设置中去。
完成上面全局配置,现在来进行token(TokenAuthentication)的认证模式。
将这个注册到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数据保存在数据库中,官方文档告诉我们如何使用这个token。header中的键是前面的Authorization,值是后面的。
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
然后现在用Postman软件的header中添加Authorization键值对,请求,请求来到中间件,每一个中间件都是会对数据进行处理。 要想获取request.user和request.auth还要在settings中添加:
#配置用户认证 REST_FRAMEWORK = { ‘DEFAULT_AUTHENTICATION_CLASSES‘: [ ‘rest_framework.authentication.BasicAuthentication‘, ‘rest_framework.authentication.SessionAuthentication‘, ‘rest_framework.authentication.TokenAuthentication‘, ] }
这样就能获取request.user与request.auth啦,但这样DRF的token存在缺点:(1)保存在数据库中,如果是一个分布式服务器,会比较麻烦。(2)token永久有效,没有一个过期时间。
2、json web token原理
前后端分离之JWT用户认证,前后端分离为啥要认证呢,因为HTTP协议是无状态协议,为了不麻烦,因此我们只需要验证用户是否是登录状态,在传统的方式,是利用Cookie和Seesion来保存用户的状态,浏览器访问将Cookie带上,然后后端验证Cookie就可以通过或者不通过,前后端分离通过RESTful API进行数据交互时,前端登陆,后端根据用户信息生成一个token,并保存token和对应用户id到数据库或Session中,接着把token传给浏览器,存入浏览器的cookie中,之后浏览器请求带上这个Cookie,后端根据这个cookie值来查询用户,验证是否过期。如果这样的话,页面出现XSS漏洞,由于Cookie可以被JS读取,XSS漏洞会导致用户token泄露(token没有过过期时间)。为了不被JS读取,可以设置httponly,设置secure,cookie就只允许通过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中重载CreateModelMixin的create函数。
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()