(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. Visual Studio 设置为 UTF-8 保存格式

    1. 步骤 Visual Studio 提供高级保存选项功能,它能指定特定代码文件的编码规范和行尾所使用的换行符. 在 VS 中,该命令没有默认显示在「文件」菜单中.用户需要手工设置,才能显示该命令. ...

  2. python学习-57 logging模块

    logging 1.basicConfig方式 import logging # 以下是日志的级别 logging.debug('debug message') logging.info('info ...

  3. Python re模块学习

    这是re模块与正则的结合 re模块提供的函数 1.match  尝试在字符串的开头应用该模式,返回匹配对象,如果没有找到匹配,则为None. import re str1 = "Why ar ...

  4. Python尾递归优化

    Python开启尾递归优化 cpython本身不支持尾递归优化, 但是一个牛人想出的解决办法:实现一个 tail_call_optimized 装饰器 #!/usr/bin/env python2.4 ...

  5. 使用docker-compose搭建WordPress

    今天博主使用typecho各种不爽,索性干掉typecho,使用WordPress 依赖 mysql nginx yml 文件 version: '3' services: nginx: image: ...

  6. PHP Math函数

    abs() 绝对值.   acos() 反余弦.   acosh() 反双曲余弦.   asin() 反正弦.   asinh() 反双曲正弦.   atan() 反正切.   atan2() 两个参 ...

  7. java 任务定时调度(定时器)

    任务定时调度 通过Timer和Timetask,我们可以实现定时启动某个线程. java.util.Timer 在这种实现方式中,Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线 ...

  8. 使用docker搭建reids主从,哨兵。

    Redis主从配置,如果没有真机就要用虚拟机,使用Docke for Windows host网络有问题. 准备: 1.安装虚拟机. 2.下载redis的安装文件:http://download.re ...

  9. MySQL中You can't specify target table '表名'('sn_app_label') for update in FROM clause错误解决办法

    在有些时候有级联关系的数据放在了同一张表中,在写sql语句的时候可能会遇到这样的场景:我要插入两条数据,第一条是父节点,第二条是子节点,关联关系是父节点的自增长id:在写这样的sql语句时有可能就会出 ...

  10. 【转载】 Sqlserver使用Left函数从最左边开始截取固定长度字符串

    在Sqlserver数据库的字符串操作中,截取字符串是最常见的操作,sql server提供了3个常用截取字符串方法,LEFT().RIGHT().SUBSTRING(),如果从第一个字符即最左边位置 ...