(PS:部分代码和图片来自博客:http://www.cnblogs.com/derek1184405959/p/8813641.html。有增删)

一、用户登录和手机注册

1、drf的token功能

(前言:为什么有了session了,还要用token呢?因为每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。)
之前用django做的网站登录都会加上csrf-token防止跨站攻击,但此次的项目是前后端分离的,而且用户可以选择在手机上登陆,就不能限制跨站登陆了。而一开始我们创建超级用户后,为什么可以登录drf?是因为我们在urls.py配置了"path('api-auth/', include('rest_framework.urls')),",点进去查看源码,如下:

可以知道drf用的登陆还是基于csrf的模式,因此这里就不合适我们这个前后端分离的项目。因此我们需要用其他的用户认证模式,这些在drf的官方文档里都有说明,drf提供给我们的auth有三种。

首先在setting.py中配置(不填写也会默认配置)

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',#浏览器中常用这种机制,因为浏览器会自动生成cookie和session返回给后端。但在前后端分离系统中很少用这个
)
}

2、开始配置token

(1)INSTALL_APP中添加

INSTALLED_APPS = (
...
'rest_framework.authtoken'
)

token会生成一张表authtoken_token,所以要运行migrations和migrate

表的作用:我们现在用的是token的认证模式,在这张表中,只要创建了一个新的用户,就会生成一个与用户对应的token

(2)url配置

from rest_framework.authtoken import views

urlpatterns = [
# token
path('api-token-auth/', views.obtain_auth_token)
]

(3)这时我们可以用浏览器Firefox,安装个插件Httprequest,来模仿登陆,测试一下这个接口

如图,输入接口url,在下方用json格式写上我们要注册的用户名和密码,用POST方式,就可获得生成返回的token。

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是在全局的情况下的。当token无效或者填错的时候,会返回一个401的错误,本来这个是没什么问题的,但是当我们访问的是商品的列表页,这种是用户在登陆和未登录的情况下都可以访问的,如果这时我们因为token过期无效等情况而返回401错误会显得很奇怪。这个问题后端的解决办法可以是取消全局配置,然后在view里面导入from rest_framework.authentication import TokenAuthentication  在需要进行token认证的class里面写上 authentication_classes = (TokenAuthentication, ))

二、json web token(jwt)方式完成用户认证

(jwt介绍:https://www.jianshu.com/p/180a870a308a、https://ruiming.me/authentication-of-frontend-backend-separate-application/)

1、jwt的基本使用

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

2、vue和jwt接口调试

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
}

3、云片网发送短信验证码

(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("", "手机号码")

4、drf实现发送短信验证码接口

手机号验证:

  • 是否合法
  • 是否已经注册

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

开始验证

5、user serializer 和validator验证

完成注册的接口

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

测试代码:

6、django信号量实现用户密码修改

(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里面,而是引入另一种方式,即信号量

2、信号量

(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启动时被运行

现在添加用户的时候,密码就会自动加密存储了

7、vue和注册功能联调

前端页面注册后,一般有两种模式,一种是跳转到登陆页面由用户自己填写登陆用户名和密码,一种是自动帮用户登录。下面介绍第二种方法的实现。

首先这里前端已经写好了登陆逻辑,不用我们管,我们只需要提供一个“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即可。

Django REST framework+Vue 打造生鲜电商项目(笔记四)的更多相关文章

  1. Django REST framework+Vue 打造生鲜电商项目(笔记二)

    (转自https://www.cnblogs.com/derek1184405959/p/8768059.html)(有修改) 接下来开始引入django resfulframework,体现它的强大 ...

  2. Django REST framework+Vue 打造生鲜电商项目(笔记十)

    (from:https://www.cnblogs.com/derek1184405959/p/8877643.html  有修改) 十三.首页.商品数量.缓存和限速功能开发 首先把pycharm环境 ...

  3. Django REST framework+Vue 打造生鲜电商项目(笔记九)

    (from:http://www.cnblogs.com/derek1184405959/p/8859309.html) 十二.支付宝沙箱环境配置 12.1.创建应用 进入蚂蚁金服开放平台(https ...

  4. Django REST framework+Vue 打造生鲜电商项目(笔记三)

    (PS:转载自http://www.cnblogs.com/derek1184405959/p/8810591.html  有修改) 一.drf的过滤 (1)添加到app里面 INSTALLED_AP ...

  5. Django REST framework+Vue 打造生鲜电商项目(笔记十一)

    (form: http://www.cnblogs.com/derek1184405959/p/8886796.html 有修改) 十四.social_django 集成第三方登录 1.申请应用 进入 ...

  6. Django REST framework+Vue 打造生鲜电商项目(笔记八)

    (form:http://www.cnblogs.com/derek1184405959/p/8862569.html) 十一.pycharm 远程代码调试 第三方登录和支付,都需要有服务器才行(回调 ...

  7. Django REST framework+Vue 打造生鲜电商项目(笔记七)

    十.购物车.订单管理和支付功能 1.添加商品到购物车 (1)trade/serializer.py 这里的serializer不继承ModelSerializer,是因为自己写的Serializer更 ...

  8. Django REST framework+Vue 打造生鲜电商项目(笔记一)

    首先,这系列随笔是我个人在学习Bobby老师的Django实战项目中,记录的觉得对自己来说比较重要的知识点,不是完完整整的项目步骤过程....如果有小伙伴想找完整的教程,可以看看这个(https:// ...

  9. Django REST framework+Vue 打造生鲜电商项目(笔记六)

    (部分代码来自https://www.cnblogs.com/derek1184405959/p/8836205.html) 九.个人中心功能开发 1.drf的api文档自动生成 (1) url #d ...

随机推荐

  1. mac upgrade node and npm

    一直以来, 我们都可以很轻松的更新npm: npm install npm -g 而Node我却是很久没有更新了, 记得当时好像是使用安装包安装的, 实际上有更加简单的安装方法. 实际上Mac上有一个 ...

  2. [转帖]商用数据库之死:Oracle 面临困境

    商用数据库之死:Oracle 面临困境 投递人 itwriter 发布于 2019-10-20 08:22 评论(1) 有238人阅读 原文链接 [收藏] « » https://news.cnblo ...

  3. tp5 关键字模糊查询 日期查询 小于大于某范围等查询的优点

    挺不错,用熟了这tp5封装的很方便. 类似上边一个查询多个操作,基本在model 一个方法搞定代码也不用很多, 首先要学会用scope  网上搜tp scope 有几个例子可以借鉴 model 内添加 ...

  4. Python12之列表3(列表常用操作符,以及常用的内置函数)

    一.比较操作符: list1 > list2             比较两个列表的大小,Python会直接从列表的第0个元素进行比较,若相等,则继续下一个比较 得到的结果是布尔值 二.逻辑操作 ...

  5. csdn博客整理

    @TOC 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页.如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown ...

  6. Mybatis之关联关系(一对多、多对多)

    目的: Mybatis关系映射之一对多 Mybatis关系映射之多对多 Mybatis关系映射之一对多 一对多 (订单对应多个订单项) 多对一  (订单项对应一个订单) 其是映射关系的基层思维是一样的 ...

  7. Mybatis配置、逆向工程自动生成代码(CRUD案例)

    目的: mybatis简介 搭建mybatis环境 基于SSM逆向工程的使用 Mybatis增删改查案例 mybatis简介 MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及 ...

  8. Jmeter之分布式测试/压测

    Jmeter做分布式测试的原因: 测试机器的配置低,对服务器进行压测时,造成不了压力. jmeter并发10000后,测试机就已经卡顿了,而且测试结果有大量失败(忽略了jmeter自身问题=.=||| ...

  9. 是否应该学习qt源码(碰到问题的时候,或者文档对函数描述不清楚的时候,可以看一下)

    是否应该学习qt源码 如果你想调用某个函数,但是文档并没有清晰描述这个函数的功能的时候,你就需要去阅读源码,看看Qt究竟是怎么实现的.比如用QNetworkAccessManager发送一个QHttp ...

  10. ASP.NET Core利用拦截器 IActionFilter实现权限控制

    “麦荻网教系统”采用了前后端代码分离的架构,即“Miidy.Cloud.Console”站与“Miidy.Cloud.Manage”站(两个前端站)同时通过web api的方式调用“Miidy.Clo ...