(PS:部分代码和图片来自博客:http://www.cnblogs.com/derek1184405959/p/8813641.html。有增删)html
(前言:为何有了session了,还要用token呢?由于每次认证用户发起请求时,服务器须要去建立一个记录来存储信息。当愈来愈多的用户发请求时,内存的开销也会不断增长。)
以前用django作的网站登陆都会加上csrf-token防止跨站攻击,但这次的项目是先后端分离的,并且用户能够选择在手机上登录,就不能限制跨站登录了。而一开始咱们建立超级用户后,为何能够登陆drf?是由于咱们在urls.py配置了"path('api-auth/', include('rest_framework.urls')),",点进去查看源码,以下:前端
能够知道drf用的登录仍是基于csrf的模式,所以这里就不合适咱们这个先后端分离的项目。所以咱们须要用其余的用户认证模式,这些在drf的官方文档里都有说明,drf提供给咱们的auth有三种。vue
首先在setting.py中配置(不填写也会默认配置)ios
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication',#浏览器中经常使用这种机制,由于浏览器会自动生成cookie和session返回给后端。但在先后端分离系统中不多用这个 ) }
(1)INSTALL_APP中添加web
INSTALLED_APPS = ( ... 'rest_framework.authtoken' )
token会生成一张表authtoken_token,因此要运行migrations和migrate正则表达式
表的做用:咱们如今用的是token的认证模式,在这张表中,只要建立了一个新的用户,就会生成一个与用户对应的token数据库
(2)url配置django
from rest_framework.authtoken import views urlpatterns = [ # token path('api-token-auth/', views.obtain_auth_token) ]
(3)这时咱们能够用浏览器Firefox,安装个插件Httprequest,来模仿登录,测试一下这个接口json
如图,输入接口url,在下方用json格式写上咱们要注册的用户名和密码,用POST方式,就可得到生成返回的token。axios
token值会保存到数据中,跟这个用户相关联
(4)客户端身份验证
在后端生成token后,并返回给前端,接下来就是前端要怎么利用这个token值了。(就是带上token值,模拟用户登陆)
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b # 注意Token后面的空格
测试方法就是点击插件的Headers,按上面我写的那样要求,填入数据。
咱们要把token放在header里面,其中key为"Authorization","Token"和它的值为value。但会发现debug模式下返回的用户信息为空
由于如今咱们用到了token的认证方式,若是想获取用户信息,就还要在setting.py里配置一下:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication' # 新增 ) }
这些配置的做用和setting.py里面默认配置的
MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', .... ]
同样,当request映射到views.py以前,django和drf会自动调用"SessionAuthentication"或"TokenAuthentication"等这些类里面的authenticate方法,这种方法会把User放到request当中去。这三种方法(指的是"BasicAuthentication"、"SessionAuthentication"、"TokenAuthentication")是用户不一样类别的验证,并且会逐一验证,只要任意一种方法获取到User,都会放到request里面。
(小Tips:前端传到后端的数据,都是保存在data里面的,但token是保存在auth里面,若是想要处理前端传回来的token,就用request.auth取出来)
咳咳,接下来进入源(zhuang)码(bi)分(bu)析(fen),不感兴趣的能够跳过这一部分。
首先为何咱们在header里面设置了token以后,经过request就能够取到User了呢?先来分析一波django从请求到相应的过程。根据django源码"site-packages/django/contrib/session/middleware.py",如图:
其中,在request响应到view以前,settings.py里面的MIDDLEWARE = [...]全部的xxxMiddleware方法都会去重载这两个方法(图中我圈出来那个),固然每一个方法放在不一样的包里面,但方法名同样。如今咱们只来分析SessionMiddleware,根据图中代码中的"def process_request(self, request)"方法,咱们知道它的做用就是把session放到request里面而已。
(推荐下关于django从请求到响应的过程的文章:http://projectsedu.com/2016/10/17/django从请求到返回都经历了什么/)
这种“请求到达Request Middlewares,中间件对request作一些预处理或者直接response请求”的好处有好比咱们能够自定义只有特定的浏览器才能够访问,做为全局拦截器,咱们能够自定义一个Middlewares,而后在“def process_request”方法里判断该是否为chorm浏览器,若是不是就返回一个HttpResponce,这样就不会进入view里面。
总的来讲,就是经过"def process_request"拦截cookies,放到request.session里面。接下来还有一步,就是经过“contrib/auth/middleware.py”,如图:
以上这些是django的MIDDLEWARE = []中的验证。
实际上MIDDLEWARE = []和REST_FRAMEWORK = {}中的验证不同,MIDDLEWARE会对每个request都作一个处理,而REST_FRAMEWORK中的'DEFAULT_AUTHENTICATION_CLASSES'是用来验证用户信息的。
接下来讲说token,为何在header加上token就能够把User取出来呢?drf里面的"rest_framework/authentication.py"的源码以下:
通过一系列判断处理以后,会拿到token值,而后传给“def authenticate_credentials(self, key)”方法
可是咱们最开始登录注册的时候,这张表是空表,那这个token是如何填充进来的呢?这时要从urls.py里面去看
点进去查看,
以上,就是token实现过程的流程。
drf的token缺点
(这里还有个小坑,就是咱们配置token是在全局的状况下的。当token无效或者填错的时候,会返回一个401的错误,原本这个是没什么问题的,可是当咱们访问的是商品的列表页,这种是用户在登录和未登陆的状况下均可以访问的,若是这时咱们由于token过时无效等状况而返回401错误会显得很奇怪。这个问题后端的解决办法能够是取消全局配置,而后在view里面导入from rest_framework.authentication import TokenAuthentication 在须要进行token认证的class里面写上 authentication_classes = (TokenAuthentication, ))
(jwt介绍:https://www.jianshu.com/p/180a870a308a、https://ruiming.me/authentication-of-frontend-backend-separate-application/)
(1)安装
pip install djangorestframework-jwt
(2)使用
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', ) }
(3)url
# jwt的token认证接口 path('jwt-auth/', obtain_jwt_token )
(4)Httprequest
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/
vue中登陆接口是login
//登陆 export const login = params => { return axios.post(`${local_host}/login/`, params) }
后台的接口跟前端要一致
urlpatterns = [ # jwt的认证接口 path('login/', obtain_jwt_token ) ]
如今就能够登陆了
jwt接口它默认采用的是Django的auth进行用户名和密码登陆验证,若是用手机号码登陆的话,就会验证失败,由于默认只能够验证username和password,因此咱们须要自定义一个用户验证,让它能够知足手机号码验证等需求
自定义用户认证
(1)settings中配置
AUTHENTICATION_BACKENDS = ( 'users.views.CustomBackend', )
(2)users/views.py
# users.views.py 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() class CustomBackend(ModelBackend): """ 自定义用户验证 """ def authenticate(self, 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
(3)JWT有效时间设置
settings中配置
import datetime #有效期限 JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), #也能够设置seconds=20 'JWT_AUTH_HEADER_PREFIX': 'JWT', #JWT跟前端保持一致,好比“token”这里设置成JWT }
(1)注册
“开发认证”-->>“签名管理”-->>“模板管理”
还要添加iP白名单,测试就用本地ip,部署的时候必定要换成服务器的ip
(2)发送验证码
apps下新建utils文件夹。再新建yunpian.py,代码以下:
# apps/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): #须要传递的参数 parmas = { "apikey": self.api_key, "mobile": mobile, "text": "【慕雪生鲜超市】您的验证码是{code}。如非本人操做,请忽略本短信".format(code=code) } response = requests.post(self.single_send_url, data=parmas) re_dict = json.loads(response.text) return re_dict if __name__ == "__main__": #例如:9b11127a9701975c734b8aee81ee3526 yun_pian = YunPian("2e87d1xxxxxx7d4bxxxx1608f7c6da23exxxxx2") yun_pian.send_sms("2018", "手机号码")
手机号验证:
(1)settings.py
# 手机号码正则表达式 REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"
(2)users下新建serializers.py,代码以下:
注意,这里对手机号码的验证代码中,为何SmsSerializer不直接用ModelSerializer继承 VerifyCode呢?是由于在 VerifyCode里面code是必填字段,而咱们这里只对mobile进行验证
# users/serializers.py import re from datetime import datetime, timedelta from MxShop.settings import REGEX_MOBILE from users.models import VerifyCode from rest_framework import serializers from django.contrib.auth import get_user_model User = get_user_model() class SmsSerializer(serializers.Serializer): mobile = serializers.CharField(max_length=11) #函数名必须:validate + 验证字段名 def validate_mobile(self, mobile): """ 手机号码验证 """ # 是否已经注册 if User.objects.filter(mobile=mobile).count(): raise serializers.ValidationError("用户已经存在") # 是否合法 if not re.match(REGEX_MOBILE, mobile): raise serializers.ValidationError("手机号码非法") # 验证码发送频率 #60s内只能发送一次 one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0) if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count(): raise serializers.ValidationError("距离上一次发送未超过60s") return mobile
(3)APIKEY加到settings里面
#云片网APIKEY APIKEY = "xxxxx327d4be01608xxxxxxxxxx"
(4)views后台逻辑
咱们要重写CreateModelMixin的create方法,下面是源码:
class CreateModelMixin(object): """ Create a model instance. """ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() def get_success_headers(self, data): try: return {'Location': str(data[api_settings.URL_FIELD_NAME])} except (TypeError, KeyError): return {}
须要加上本身的逻辑
users/views.py
from rest_framework.mixins import CreateModelMixin from rest_framework import viewsets from .serializers import SmsSerializer from rest_framework.response import Response from rest_framework import status from utils.yunpian import YunPian from MxShop.settings import APIKEY from random import choice from .models import VerifyCode
# 对VerifyCode的操做,如发送一条验证码就把手机号码和验证码保存在表里,至关于
# create操做,因此要继承CreateModelMixin class SmsCodeViewset(CreateModelMixin,viewsets.GenericViewSet): ''' 手机验证码 ''' serializer_class = SmsSerializer # "serializer_class"是固定的,这样写,结合下面的"def create()"方法,就会自动进行验证 def generate_code(self): """ 生成四位数字的验证码 """ 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"] 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)
云片网单条短信发送的使用说明:
(5)配置url
from users.views import SmsCodeViewset # 配置codes的url router.register(r'code', SmsCodeViewset, base_name="code")
开始验证
完成注册的接口
(1)修改UserProfile中mobile字段
mobile = models.CharField("电话",max_length=11,null=True, blank=True)
设置容许为空,由于前端只有一个值,是username,因此mobile能够为空
(2)users/serializers.py
注意:这里使用的是“ModelSerializer”,跟上面的“SmsSerializer”继承“Serializers”不同,虽然UserProfile里面也没有code字段,但这里主要介绍的是当使用“ModelSerializer”时的解决办法
class UserRegSerializer(serializers.ModelSerializer): ''' 用户注册 ''' #UserProfile中没有code字段,这里须要自定义一个code序列化字段 code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, error_messages={ "blank": "请输入验证码", "required": "请输入验证码", "max_length": "验证码格式错误", "min_length": "验证码格式错误" }, help_text="验证码") #验证用户名是否存在 username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False, validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")]) #验证code def validate_code(self, code): # 用户注册,已post方式提交注册信息,post的数据都保存在initial_data里面 #username就是用户注册的手机号,验证码按添加时间倒序排序,为了后面验证过时,错误等 verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time") if verify_records: # 最近的一个验证码 last_record = verify_records[0] # 有效期为五分钟。 five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) if five_mintes_ago > last_record.add_time: raise serializers.ValidationError("验证码过时") if last_record.code != code: raise serializers.ValidationError("验证码错误") else: raise serializers.ValidationError("验证码错误") # 全部字段。attrs是字段验证合法以后返回的总的dict def validate(self, attrs): #前端没有传mobile值到后端,这里添加进来 attrs["mobile"] = attrs["username"] #code是本身添加得,数据库中并无这个字段,验证完就删除掉 del attrs["code"] return attrs class Meta: model = User fields = ('username','code','mobile') # 由于User指的就是Userprofile,而Userprofile继承自Django的User,因此这里username是必填字段
(3)users/views.py
class UserViewset(CreateModelMixin,viewsets.GenericViewSet): ''' 用户 ''' serializer_class = UserRegSerializer
(4)配置url
router.register(r'users', UserViewset, base_name="users")
测试代码:
(1)完善用户注册
user/views.py
class UserViewset(CreateModelMixin,viewsets.GenericViewSet): ''' 用户 ''' serializer_class = UserRegSerializer queryset = User.objects.all()
user/serializer.py添加
fields = ('username','code','mobile','password')
(2)password不能明文显示和加密保存
须要重载Create方法
# “write_only=True”,不会返回到前端 password = serializers.CharField( style={'input_type': 'password'},label=“密码”,write_only=True ) #密码加密保存 def create(self, validated_data): user = super(UserRegSerializer, self).create(validated_data=validated_data) user.set_password(validated_data["password"]) user.save() return user
固然,上面的须要重载Create方法而后对密码加密保存咱们能够不写在UserRegSerializer里面,而是引入另外一种方式,即信号量
二、信号量
(1)users下面建立signals.py
下面代码简单的解释就是监听User是否接收到以post方法传递过来的数据,若是有,判断是不是新建立用户,若是是,进行保存
# 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() # post_save:接收信号的方式 #sender: 接收信号的model @receiver(post_save, sender=User) def create_user(sender, instance=None, created=False, **kwargs): # 是否新建,由于update的时候也会进行post_save if created: password = instance.password #instance至关于user instance.set_password(password) instance.save()
(2)还须要重载配置
users/apps.py
# users/apps.py from django.apps import AppConfig class UsersConfig(AppConfig): name = 'users' verbose_name = "用户管理" def ready(self): import users.signals
AppConfig自定义的函数,会在django启动时被运行
如今添加用户的时候,密码就会自动加密存储了
前端页面注册后,通常有两种模式,一种是跳转到登录页面由用户本身填写登录用户名和密码,一种是自动帮用户登陆。下面介绍第二种方法的实现。
首先这里前端已经写好了登录逻辑,不用咱们管,咱们只须要提供一个“token”接口给前端就行,实现自动登陆
生成token的两个重要步骤,一是payload,二是encode
class UserViewset(CreateModelMixin,viewsets.GenericViewSet): ''' 用户 ''' serializer_class = UserRegSerializer 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) re_dict = serializer.data # 默认的create方法,由于返回的是serializer.data,因此把token放到里面,再返回 payload = jwt_payload_handler(user) # 这种生成token的方法是从jwt的源码里找到的 re_dict["token"] = jwt_encode_handler(payload) # 生成token 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): # 重载这个函数,由于源码里面是没有返回的,可是咱们又须要调用。注意这里的serializer指的就是 return serializer.save() # UserRegSerializer里的model = User对象
后续的登录成功后的退出,由于token是保存在客户端的,因此只须要在前端那里删除用户本地的cookie便可。