drf相关问题

drf自定义用户认证:

登录默认 使用django的ModelBackend,对用户名和密码进行验证。但我们平时登录网站时除了用户名也可以用邮箱或手机进行登录,这就需要我们自己扩展backend

一、settings中配置

AUTHENTICATION_BACKENDS = (
    'users.views.CustomBackend',
)

users.views中新建backend,对用户名或手机进行验证

二、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

  

三、JWT有效时间设置

settings中配置

import datetime
#有效期限
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),    #也可以设置seconds=20
    'JWT_AUTH_HEADER_PREFIX': 'JWT',                       #JWT跟前端保持一致,比如“token”这里设置成JWT
}

 

四 添加url

from rest_framework_jwt.views import obtain_jwt_token
#...

urlpatterns = [
    '',
    # ...

    url(r'^api-token-auth/', obtain_jwt_token),
]

  

drf的token与jwt的区别:

  • 保存在数据库中,如果是一个分布式的系统,就非常麻烦
  • token永久有效,没有过期时间。

 

Django Rest framework实现流程

目录

一 什么是restful架构
二 Django REST framework简介
三 Django REST framework原理
四 Django REST framework源码流程
五 Django REST framework实现用户登录

一 什么是restful架构

1、起源

REST即表述性状态传递(英文:Representational State Transfer,简称REST)是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。它是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。
目前在三种主流的Web服务实现方案中,因为REST模式的Web服务与复杂的SOAP和XML-RPC对比来讲明显的更加简洁,越来越多的web服务开始采用REST风格设计和实现。例如,Amazon.com提供接近REST风格的Web服务进行图书查找;雅虎提供的Web服务也是REST风格的

2、框架组成

根据rest的论文,我们大致可以把restful架构分成以下几个部分

1. 资源

资源就是网络中的具体信息。比如说招聘信息、美女图片、NBA赛事、股票信息、歌曲等等。每一种具体资源都可以用一个URI(统一资源定位符)指向它。如果想要获取这些资源,则直接访问它的URI就可以。
有很多公共的URI,例如阿里云的api市场就提供了很多种的api,每种api代替了具体的资源,每种资源你都可以通过访问api获取到。
例如访问下面图片中的api地址,就可以获取到沪深港股票历史行情数据

2. 表现方式

在资源里我们提到了阿里云提供了很多api,api获取了是一些静态资源,而表现方式就会为了处理获取到的这些资源以什么样的形式展示给访问者。
现在常用的方式有比如,txt格式、HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。
从上图可以看到,api的表现形式(也就是返回数据格式)为json格式。

3. 状态

状态定义了做资源的操作方式。这些操作方式全部定义在http协议里面,而不再api上表现。客户端通过四个HTTP动词,对服务器端资源进行操作
具体操作:

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供完整资源数据)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供需要修改的资源数据)。
  • DELETE(DELETE):从服务器删除资源。

3、认证机制

我们知道,客户端通过api可以访问到资源,但我们还需要对访问者进行验证,以用来判断该用户的访问权限。
常用的认证机制包括 session auth(即通过用户名密码登录),basic auth,token auth和OAuth,服务开发中常用的认证机制为后三者。

Basic Auth

Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。

Token Auth

Token Auth并不常用,它与Basic Auth的区别是,不将用户名和密码发送给服务器做用户认证,而是向服务器发送一个事先在服务器端生成的token来做认证。因此Token Auth要求服务器端要具备一套完整的Token创建和管理机制,该机制的实现会增加大量且非必须的服务器端开发工作,也不见得这套机制足够安全和通用,因此Token Auth用的并不多。

1. OAuth介绍

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

正是由于OAUTH的严谨性和安全性,现在OAUTH已成为RESTful架构风格中最常用的认证机制,和RESTful架构风格一起,成为企业级服务的标配。
目前OAuth已经从OAuth1.0发展到OAuth2.0,但这二者并非平滑过渡升级,OAuth2.0在保证安全性的前提下大大减少了客户端开发的复杂性。

二 Django REST framework简介

Django REST framework是一个基于Django实现的一个restful框架,一个十分强大切灵活的工具包,用以构建Web APIs。
Djando REST framework的优点:

  • 在线可视的API
  • 验证策略涵盖了OAuth1和OAuth2
  • 同时支持ORM和非ORM数据源的序列化
  • 支持基于类的视图

安装与部署环境

使用pip安装框架以及所有依赖包

pip install djangorestframework
pip install markdown  # 可以更好的支持可浏览的API
pip install django-filter  # 支持过滤

还可以使用clone安装
git clone git@github.com:encode/django-rest-framework.git
安装完之后需要在项目的配置文件的app里面注册rest_framework,因为有些地方会用到rest_framework这个app里面的数据,例如返回数据的模板

INSTALLED_APPS= (
    ...
    'rest_framework',

)

三 Django REST framework简单流程

首先我们看一个通过Django REST framework实现的一个api的简单实例

# views.py 首先写视图函数
from rest_framework.views import APIView
from rest_framework.response import Response


class TestView(APIView):  # CBV模式的视图函数

    def get(self, request, *args, **kwargs):
        # 定义get方法
        # 在django-rest-framework中的request被重新封装了,后续分析源码的时候会有具体体现
        return Response('测试api')  # rest-framework的模板对数据进行渲染

# urls.py 定义路由
from django.conf.urls import url
from django.contrib import admin

from app01 import views

urlpatterns = [
    url(r'^user/', views.TestView.as_view())
]

启动项目,访问api:

原理分析:

  1. rest_framework重写了一个APIView的类,这个类是之前django中的view类的派生类,在写CBV的视图函数是,让视图函数继承了rest-framework这个APIView类
  2. 在视图函数中写具体方法,例如get,port,put,delete,可以使用rest_framework的Response对返回数据进行渲染
  3. 在url的写法和Django是一样的
  4. 通过对http://127.0.0.1:8000/user/ 的访问,可以得到如图显示的数据

四 Django REST framework源码流程

在第三节我们分析了一个简单的api实现过程,现在我们主要去分析rest_framework内部对这个url的具体实现过程。

  1. 首先我们访问http://127.0.0.1:8000/user/ 根据urls.py中的配置,执行views.TestView.as_view()函数
  2. as_view方法是被定义在rest_framework/views.py里面的一个静态方法,所以可以通过类名直接调用。

  3. 父类的as_view方法是定义在django/views/generic/base.py里面的View类中的方法。在这个方法中最终会执行cls.dispatch,在第一步中我们知道cls是<class 'app01.views.TestView'>

  4. dispatch是定义在TestView继承的父类APIView(rest_framework/views.py)里面的方法。在这个方法里面,首先通过 request = self.initialize_request(request, *args, **kwargs)这条语句重新封装了request对象,这两行代码,重中之重,实现了对request方法的封装和重写。为什么要重写request呢?

      因为在Django中,信息的解析与封装是通过wsgiref这个模块实现的,这个模块有一个缺陷。封装的request.POST方法不支持json格式的字符串,也就是说这个模块中没有提供json格式的解析器。而前后端的分离,就需要一种前后端都支持的数据格式,那就是json,而这恰好是wsgiref所无法实现的,要实现前后端分离,必须要对request方法重写。

      rest_framework是如何实现request的重写的。self.initialize_request(request, *args, **kwargs)里面都实现了那些东西?

  5. initialize_request是APIView类里面的一个方法,重新封装了request对象,增加了一些属性信息这个方法return了一个类Request的实例对象,这个Request类传入旧的request,对旧的equest进行了一系列的加工,返回新的request对象

     

  6. 这个方法return了一个类Request的实例对象,这个Request类传入旧的request,对旧的equest进行了一系列的加工,返回新的request对象

     

  7. 初始化的过程中,我们可以得到一些对我们有用的信息,我们可以通过这个类Request的实例对象,也就是新的request,通过

    request._request 就可以得到我们旧的request。

    所以,我们可以通过这个新的request.query_params 得到旧的request.GET的值(这个方法没啥卵用),最关键的是request.data这个方式,可以得到序列化后的数据,这个方法补全了旧request的不足。

      备注:在request.data中,提供了很多数据类型的解析器,包括json的,所以,对于提交的数据,我们可以直接通过这个方法获取到。而不需要通过request.POST

     

     

  8. 认证信息。主要通过APIView类中的get_authenticators(rest_framework/views.py)方法获取,这个方法会返回一个所有认证对象的列表
    在全局定义的authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

  9. 默认的认证配置信息是在rest_framework/settings.py文件中定义的

  10. 在rest_framework/authentication.py中定义了几种认证类型,一般情况我们需要自定义认证类,也可以使用django-oauth-toolkit组件进行认证。

  11. dispatch中的initialize_request方法执行完成之后,还有执行一个重要方法是self.initial(request, *args, **kwargs),这个方法也是APIView类里的。在这个方法里面初始化
    被重新封装的request对象
    实现功能:
    • 版本处理
    • 用户认证
    • 权限
    • 访问频率限制

  12. 执行APIView里面的perform_authentication方法,该方法返回request.user,则会调用<rest_framework.request.Request object at 0x10e80deb8>里面的user方法。在user方法里面最终调用了Request类里面的_authenticate方法


  13. 执行rest_framework.request.Request类中的_authenticate方法,这个方法会遍历认证类,并根据认证结果给self.user, self.auth赋值。由于user,和auth都有property属性,
    所以给赋值的时候先在先执行setter方法


  14. dispatch中的initial方法执行完之后,会继续判断request.method并执行method相应的method.

    目录 

    限流实现流程

    一 什么是throttle
    二 Django REST framework是如何实现throttle的
    三 Django REST framework中throttle源码流程

    一 什么是throttle

    节流也类似于权限,它用来决定一个请求是否被授权。节流表示着一种临时的状态,常常用来控制客户端对一个
    API的请求速率。例如,你可以通过限流来限制一个用户在每分钟对一个API的最多访问次数为60次,每天的访问次数为1000次。
     

    二 Django REST framework是如何实现throttle的

    1. 在Django REST framework中主要是通过throttling.py文件里面的几个类来实现限流功能的。

    2. 在整个流程上是在dispatch中的调用的initial方法中的self.check_throttles(request)调用到throttle类中的方法

    3. throttle策略的配置:
      全局配置settings.py
    REST_FRAMEWORK = {
        'DEFAULT_THROTTLE_CLASSES': (  # 定义限流类
            'rest_framework.throttling.AnonRateThrottle',
            'rest_framework.throttling.UserRateThrottle'
        ),
        'DEFAULT_THROTTLE_RATES': {   # 定义限流速率,支持秒、分、时、天的限制
            'anon': '100/day',
            'user': '1000/day'
        }
    }
    1. 把限流策略设置在视图函数上
      CBV
    from rest_framework.response import Response
    from rest_framework.throttling import UserRateThrottle
    from rest_framework.views import APIView
    class ExampleView(APIView):
        throttle_classes = (UserRateThrottle,)
        def get(self, request, format=None):
            content = {
                'status': 'request was permitted'
            }
            return Response(content)

    FBV

    @api_view(['GET'])
    @throttle_classes([UserRateThrottle])
    def example_view(request, format=None):
        content = {
            'status': 'request was permitted'
        }
        return Response(content)

     

    三 Django REST framework中throttle源码流程

    1. 调用check_throttles方法,在这个方法中会遍历通过self.get_throttles()获取到的限流对象列表,默认列表里面是空的。也就是说默认不会有限流的策略。

    2. 在视图函数里面配置参数,让其应用上限流策略。我们这里以UserRateThrottle这个限流方法为例。(配置如第二节中的settings.py配置和FBV配置),在这里继续第二步的操作,执行UserRateThrottle对象的allow_request方法。
      由于UserRateThrottle这个类本身没有allow_request方法,所以在其父类SimpleRateThrottle中找这个方法.

    3. 执行allow_request方法,会首先判断是否定义了self.rate。根据self.rate执行,最终回去查找self.scope属性,而且这个属性是必须的。

    4. 在UserRateThrottle中查找到定义的scope="user", 接着执行self.key语句。这条语句最终调用了UserRateThrottle里面的get_cache_key方法。
      此例中我们没有配置authenticate,所有会执行get_cache_key里面的get_indet方法,并最终返回了scope和ident被self.key接收(返回格式:throttle_user_127.0.0.1)。

    5. 返回self.key之后继续执行self.history,self.history会返回客户端的访问记录列表,并根据rate的配置去判断是否是要pop某一条访问记录。并最终根据列表长度和允许的长度做对比,判断客户端当前是否具有访问权限。

    6. 若最终check_throttles返回True,则继续执行dispatch。dispatch之后的操作请参考之前写的django rest framework流程。如果返回False,则会继续执行self.throttled(request, throttle.wait())。

    7. 执行父类SimpleRateThrottle里面的wait方法。这个方法主要用来向客户端返回还需要多少时间可以继续访问的提示。

     

    组件二、serializers

     开发我们的Web API的第一件事是为我们的Web API提供一种将代码片段实例序列化和反序列化为诸如json之类的表示形式的方式。

      我们在使用json序列化时,有这么几种方式:

      第一种,借助Python内置的模块j。手动构建一个字典,通过json.dumps(obj)得到一个json格式的字符串,使用这种方式有一定的局限性,那就是我们无法控制这张表中的字段,有多少个字段,我们就需要添加多少个键值对,一旦后期表结构发生变化,就会很麻烦。

      第二种方式:解决上述方式中字段的不可控性,就需要借助Django中内置的一个方法model_to_dict,(from django.forms.models import model_to_dict),我们可以将取出的所有的字段循环,依次将每个对象传入model_to_dict中,这样就解决了字段的问题。

      还有一种方式,也是Django提供的一个序列化器:from django.core.serializers import serialize   ,我们可以直接将queryset数据类型直接传进去,然后指定我们要转化的格式即可,这两种Django提供给我们的方法虽然可以解决这个序列化字段的问题,但是有一个缺点,那就是我们可以直接将一个数据转化为json字符串形式,但是却无法反序列化为queryset类型的数据。

      rest_framework提供了一个序列化组件---serializers,完美的帮助我们解决了上述的问题:from rest_framework import serializers ,用法跟Django中forms组件的用法非常相似,也需要先自定制一个类,然后这个类必须继承serializers.Serializer,然后我们需要序列化那些字段就在这个类中配置那些字段,还可以自定制字段的展示格式,非常的灵活。

    models部分:

    from django.db import models
    
    # Create your models here.
    
    
    class Book(models.Model):
        title=models.CharField(max_length=32)
        price=models.IntegerField()
        pub_date=models.DateField()
        publish=models.ForeignKey("Publish")
        authors=models.ManyToManyField("Author")
        def __str__(self):
            return self.title
    
    class Publish(models.Model):
        name=models.CharField(max_length=32)
        email=models.EmailField()
        def __str__(self):
            return self.name
    
    class Author(models.Model):
        name=models.CharField(max_length=32)
        age=models.IntegerField()
        def __str__(self):
            return self.name

    views部分:

    from rest_framework.views import APIView
    from rest_framework.response import Response
    from .models import *
    from django.shortcuts import HttpResponse
    from django.core import serializers
    
    
    from rest_framework import serializers
    
    class BookSerializers(serializers.Serializer):
        title=serializers.CharField(max_length=32)
        price=serializers.IntegerField()
        pub_date=serializers.DateField()
        publish=serializers.CharField(source="publish.name")
        #authors=serializers.CharField(source="authors.all")
        authors=serializers.SerializerMethodField()
        def get_authors(self,obj):
            temp=[]
            for author in obj.authors.all():
                temp.append(author.name)
            return temp
    
    
    class BookViewSet(APIView):
    
        def get(self,request,*args,**kwargs):
            book_list=Book.objects.all()
            # 序列化方式1:
            # from django.forms.models import model_to_dict
            # import json
            # data=[]
            # for obj in book_list:
            #     data.append(model_to_dict(obj))
            # print(data)
            # return HttpResponse("ok")
    
            # 序列化方式2:
            # data=serializers.serialize("json",book_list)
            # return HttpResponse(data)
    
            # 序列化方式3:
            bs=BookSerializers(book_list,many=True)
            return Response(bs.data)

      值得注意的时,我们使用这种方式序列化时,需要先实例化一个对象,然后在传值时,如果值为一个queryset对象时,需要指定一个参数many=True,如果值为一个obj时,不需要指定many=False,默认为False。

     
    #我们自定义一个类Bookserialize,继承serializers.Serializer
    
    from framework.views import Bookserialize
    from framework import models
    book_list = models.Book.objects.all()
    bs= Bookserialize(book_list,many=True)
    bs.data
    [OrderedDict([('title', '神墓')]), OrderedDict([('title', '完美世界')])]
    # queryset对象  结果为一个列表,里面放着每一个有序字典
    book_obj = models.Book.objects.first()
    bs_ = Bookserialize(book_obj)
    bs_.data
    {'title': '神墓'}
    # 如果为一个对象时,结果为一个字典
     

    特别需要注意的是:使用这种方式序列化时,对于特殊字段(一对多ForeignKey、多对多ManyToMany),serializers没有提供对应的字段,需要指定特殊的方式,因为obj.这个字段时,得到的是一个对象,所以我们对于FK,需要使用一个CharField字段,然后在这个字段中指定一个source属性,指定显示这个对象的那个字段。同样的,对于多对多的字段,我们也要使用特殊的显示方式:SerializerMethodField(),指定为这种字段类型时,显示的结果为一个自定义的函数的返回值,这个自定义函数的名字必须是get_字段名,固定写法,接收一个obj对象,返回值就是该字段在序列化时的显示结果。

      另外,我们在取值时,直接通过这个对象.data的方式取值,这是rest_framework提供给我们的序列化接口。

      其实。我们应该明白它内部的实现方式:如果值为一个queryset对象,就创建一个list,循环这个queryset得到每一个数据对象,然后在循环配置类下面的每一个字段,直接这个对象obj.字段  得出值,添加到一个字典中,在将这个字典添加到这个列表中。所以,对于这些特殊字段,我们取值时,通过这种方式得到的是一个对象。

      通过这种方式就会出现一个问题,我们每序列化一个表,就要将这个表中的字段全部写一遍,这样显得很麻烦。在Djangoforms组件中,有一个ModelForm,可以帮我们将我们模型表中的所有字段转化为forms组件中对应的字段,同样的,在serializers中,同样有一个,可以帮我们将我们的模型类转化为serializers中对应的字段。这个组件就是ModelSerializer。

     

组件三、ModelSerializer

class BookSerializers(serializers.ModelSerializer):
      class Meta:
          model=Book
          fields="__all__"
          depth=1

备注:

1.定义一个depth=1  会将特殊字段  FK和M2M中每个对象的所有字段全部取出来。

2.对于FK字段,显示的是主键值,对于多对多字段,默认显示方式为:[pk1,pk2,...],一个列表中,包含所有字段对象的主键值。如果我们不想显示主键值,可以重写对应字段属性。

复制代码
class BookModelSerializers(ModelSerializer):
    class Meta:
        model=Book
        fields="__all__"

    authors=serializers.SerializerMethodField()
    def get_authors(self,obj):
         temp=[]
         for obj in  obj.authors.all():
             temp.append(obj.name)
         return temp
复制代码

 3.对于含有choices的字段,我们可以通过指定字段的source来显示展示的值

  比如:course_class = models.Integerfield(choices=((1,'初级'),(2,'中级')))

course_class = serializers.CharField(source='get_course_class_display')

提交post请求

复制代码
复制代码
  def post(self,request,*args,**kwargs):
       
        bs=BookSerializers(data=request.data,many=False)
        if bs.is_valid():
            # print(bs.validated_data)
            bs.save()
            return Response(bs.data)
        else:
            return HttpResponse(bs.errors)
复制代码
复制代码

备注:跟form组件类似,如果校验不通过,可以通过这个对象.errors将错误信息返回。

重写save中的create方法

复制代码
复制代码
class BookSerializers(serializers.ModelSerializer):

      class Meta:
          model=Book
          fields="__all__"
          # exclude = ['authors',]
          # depth=1

      def create(self, validated_data):
        
          authors = validated_data.pop('authors')
          obj = Book.objects.create(**validated_data)
          obj.authors.add(*authors)
          return obj
复制代码
复制代码

 单条数据的get和put请求

复制代码
复制代码
class BookDetailViewSet(APIView):

    def get(self,request,pk):
        book_obj=Book.objects.filter(pk=pk).first()
        bs=BookSerializers(book_obj)
        return Response(bs.data)

    def put(self,request,pk):
        book_obj=Book.objects.filter(pk=pk).first()
        bs=BookSerializers(book_obj,data=request.data)
        if bs.is_valid():
            bs.save()
            return Response(bs.data)
        else:
            return HttpResponse(bs.errors)
复制代码
复制代码

超链接API:Hyperlinked

复制代码
复制代码
class BookSerializers(serializers.ModelSerializer):
      publish= serializers.HyperlinkedIdentityField(
view_name='publish_detail',
lookup_field="publish_id",
lookup_url_kwarg="pk") class Meta: model=Book fields="__all__" #depth=1
复制代码
复制代码

urls部分:

            

1
2
3
4
5
6
urlpatterns = [
    url(r'^books/$', views.BookViewSet.as_view(),name="book_list"),
    url(r'^books/(?P<pk>\d+)$', views.BookDetailViewSet.as_view(),name="book_detail"),
    url(r'^publishers/$', views.PublishViewSet.as_view(),name="publish_list"),
    url(r'^publishers/(?P<pk>\d+)$', views.PublishDetailViewSet.as_view(),name="publish_detail"),
]

深入解析当下大热的前后端分离组件django-rest_framework系列三

 

三剑客之认证、权限与频率组件

认证组件

局部视图认证

在app01.service.auth.py:

复制代码
复制代码
复制代码
class Authentication(BaseAuthentication):

    def authenticate(self,request):
        token=request._request.GET.get("token")
        token_obj=UserToken.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed("验证失败!")
        return (token_obj.user,token_obj)
复制代码
复制代码
复制代码

在views.py:

复制代码
复制代码
复制代码
def get_random_str(user):
    import hashlib,time
    ctime=str(time.time())

    md5=hashlib.md5(bytes(user,encoding="utf8"))
    md5.update(bytes(ctime,encoding="utf8"))

    return md5.hexdigest()


from app01.service.auth import *

from django.http import JsonResponse
class LoginViewSet(APIView):
    authentication_classes = [Authentication,]
    def post(self,request,*args,**kwargs):
        res={"code":1000,"msg":None}
        try:
            user=request._request.POST.get("user")
            pwd=request._request.POST.get("pwd")
            user_obj=UserInfo.objects.filter(user=user,pwd=pwd).first()
            print(user,pwd,user_obj)
            if not user_obj:
                res["code"]=1001
                res["msg"]="用户名或者密码错误"
            else:
                token=get_random_str(user)
                UserToken.objects.update_or_create(user=user_obj,defaults={"token":token})
                res["token"]=token

        except Exception as e:
            res["code"]=1002
            res["msg"]=e

        return JsonResponse(res,json_dumps_params={"ensure_ascii":False})
复制代码
复制代码
复制代码

备注:一个知识点:update_or_create(参数1,参数2...,defaults={‘字段’:'对应的值'}),这个方法使用于:如果对象存在,则进行更新操作,不存在,则创建数据。使用时会按照前面的参数进行filter,结果为True,则执行update  defaults中的值,否则,创建。

使用方式: 

  使用认证组件时,自定制一个类,在自定制的类下,必须实现方法:authenticate(self,request),必须接收一个request参数,结果必须返回一个含有两个值的元组,认证失败,就抛一个异常rest_framework下的exceptions.APIException。然后在对应的视图类中添加一个属性:authentication_classes=[自定制类名]即可。

 备注:from rest_framework.exceptions import AuthenticationFailed   也可以抛出这个异常(这个异常是针对认证的异常)

使用这个异常时,需要在自定制的类中,定制一个方法:def authenticate_header(self,request):pass,每次这样定制一个方法很麻烦,我们可以在自定义类的时候继承一个类:BaseAuthentication

调用方式:from rest_framework.authentication import BaseAuthentication

 

为什么是这个格式,我们看看这个组件内部的 实现吧!
不管是认证,权限还是频率都发生在分发之前:

复制代码
复制代码
class APIView(View):
        def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
复制代码
复制代码

所有的实现都发生在       self.initial(request, *args, **kwargs)        这行代码中

复制代码
复制代码
class APIView(View):
     def initial(self, request, *args, **kwargs):
        """
        Runs anything that needs to occur prior to calling the method handler.
        """
        self.format_kwarg = self.get_format_suffix(**kwargs)

        # Perform content negotiation and store the accepted info on the request
        neg = self.perform_content_negotiation(request)
        request.accepted_renderer, request.accepted_media_type = neg

        # Determine the API version, if versioning is in use.
       #处理版本信息 version, scheme = self.determine_version(request, *args, **kwargs) request.version, request.versioning_scheme = version, scheme # Ensure that the incoming request is permitted
      # 认证 self.perform_authentication(request)
      # 权限 self.check_permissions(request)
      # 用户访问频率的限制 self.check_throttles(request)
复制代码
复制代码

我们先看认证相关   self.perform_authentication

复制代码
复制代码
class APIView(View):
        def perform_authentication(self, request):
        """
        Perform authentication on the incoming request.

        Note that if you override this and simply 'pass', then authentication
        will instead be performed lazily, the first time either
        `request.user` or `request.auth` is accessed.
        """
        request.user
复制代码
复制代码

整个这个方法就执行了一句代码:request.user   ,这个request是新的request,是Request()类的实例对象,  .user 肯定在Request类中有一个user的属性方法。

复制代码
复制代码
class Request(object):
    @property
    def user(self):
        """
        Returns the user associated with the current request, as authenticated
        by the authentication classes provided to the request.
        """
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user
复制代码
复制代码

这个user方法中,执行了一个 self._authenticate()方法,继续看这个方法中执行了什么:

复制代码
复制代码
class Request(object):
        def _authenticate(self):
        """
        Attempt to authenticate the request using each authentication instance
        in turn.
        """
        for authenticator in self.authenticators:
            try:
                user_auth_tuple = authenticator.authenticate(self)
            except exceptions.APIException:
                self._not_authenticated()
                raise

            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple
                return

        self._not_authenticated()
复制代码
复制代码

这个方法中,循环一个东西:self.authenticators  ,这个self,是新的request,我们看看这个self.authenticators是什么。

复制代码
复制代码
class Request(object):
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.

    Kwargs:
        - request(HttpRequest). The original request instance.
        - parsers_classes(list/tuple). The parsers to use for parsing the
          request content.
        - authentication_classes(list/tuple). The authentications used to try
          authenticating the request's user.
    """

    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
       
        self._request = request
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
复制代码
复制代码

这个self.authenticators是实例化时初始化的一个属性,这个值是实例化新request时传入的参数。

复制代码
复制代码
class APIView(View):
   
     def initialize_request(self, request, *args, **kwargs):
        """
        Returns the initial request object.
        """
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )
复制代码
复制代码

实例化传参时,这个参数来自于self.get_authenticators(),这里的self就不是request,是谁调用它既是谁,看看吧:

复制代码
复制代码
class APIView(View):
        def get_authenticators(self):
        """
        Instantiates and returns the list of authenticators that this view can use.
        """
        return [auth() for auth in self.authentication_classes]
复制代码
复制代码

这个方法,返回了一个列表推导式   [auth() for auth in self.authentication_classes] ,到了这里,是不是很晕,这个self.authentication_classes又是啥,这不是我们在视图函数中定义的属性吗,值是一个列表,里面放着我们自定义认证的类

class APIView(View):

    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

如果我们不定义这个属性,会走默认的值  api_settings.DEFAULT_AUTHENTICATION_CLASSES

  走了一圈了,我们再走回去呗,首先,这个列表推导式中放着我们自定义类的实例,回到这个方法中def _authenticate(self),当中传的值self指的是新的request,这个我们必须搞清楚,然后循环这个包含所有自定义类实例的列表,得到一个个实例对象。

 

for authenticator in self.authenticators:
try: user_auth_tuple = authenticator.authenticate(self) except exceptions.APIException: self._not_authenticated() raise if user_auth_tuple is not None: self._authenticator = authenticator self.user, self.auth = user_auth_tuple return

 

然后执行每一个实例对象的authenticate(self)方法,也就是我们在自定义类中必须实现的方法,这就是原因,因为源码执行时,会找这个方法,认证成功,返回一个元组 ,认证失败,捕捉一个异常,APIException。认证成功,这个元组会被self.user,self.auth接收值,所以我们要在认证成功时返回含有两个值的元组。这里的self是我们新的request,这样我们在视图函数和模板中,只要在这个request的生命周期,我们都可以通过request.user得到我们返回的第一个值,通过request.auth得到我们返回的第二个值。这就是认证的内部实现,很牛逼。

备注:所以我们认证成功后返回值时,第一个值,最好时当前登录人的名字,第二个值,按需设置,一般是token值(标识身份的随机字符串)

 这种方式只是实现了对某一张表的认证,如果我们有100张表,那这个代码我就要写100遍,复用性很差,所以需要在全局中定义。

全局视图认证组件

settings.py配置如下:

1
2
3
REST_FRAMEWORK={
    "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",]
}

为什么要这样配置呢?我们看看内部的实现吧:

  从哪开始呢?在局部中,我们在视图类中加一个属性authentication_classes=[自定义类],那么在内部,肯定有一个默认的值:

class APIView(View):

    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

默认值是api_settings中的一个属性,看看这个默认值都实现了啥

api_settings = APISettings(None, DEFAULTS, IMPORT_STRINGS)

首先,这个api_settings是APISettings类的一个实例化对象,传了三个参数,那这个DEFAULTS是啥,看看:

复制代码
复制代码
#rest_framework中的settings.py

DEFAULTS = {
    # Base API policies
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ),
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser'
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication'
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.AllowAny',
    ),
    'DEFAULT_THROTTLE_CLASSES': (),
    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
    'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
    'DEFAULT_VERSIONING_CLASS': None,

    # Generic view behavior
    'DEFAULT_PAGINATION_CLASS': None,
    'DEFAULT_FILTER_BACKENDS': (),

    # Schema
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',

    # Throttling
    'DEFAULT_THROTTLE_RATES': {
        'user': None,
        'anon': None,
    },
    'NUM_PROXIES': None,

    # Pagination
    'PAGE_SIZE': None,

    # Filtering
    'SEARCH_PARAM': 'search',
    'ORDERING_PARAM': 'ordering',

    # Versioning
    'DEFAULT_VERSION': None,
    'ALLOWED_VERSIONS': None,
    'VERSION_PARAM': 'version',

    # Authentication
    'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
    'UNAUTHENTICATED_TOKEN': None,

    # View configuration
    'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
    'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',

    # Exception handling
    'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
    'NON_FIELD_ERRORS_KEY': 'non_field_errors',

    # Testing
    'TEST_REQUEST_RENDERER_CLASSES': (
        'rest_framework.renderers.MultiPartRenderer',
        'rest_framework.renderers.JSONRenderer'
    ),
    'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',

    # Hyperlink settings
    'URL_FORMAT_OVERRIDE': 'format',
    'FORMAT_SUFFIX_KWARG': 'format',
    'URL_FIELD_NAME': 'url',

    # Input and output formats
    'DATE_FORMAT': ISO_8601,
    'DATE_INPUT_FORMATS': (ISO_8601,),

    'DATETIME_FORMAT': ISO_8601,
    'DATETIME_INPUT_FORMATS': (ISO_8601,),

    'TIME_FORMAT': ISO_8601,
    'TIME_INPUT_FORMATS': (ISO_8601,),

    # Encoding
    'UNICODE_JSON': True,
    'COMPACT_JSON': True,
    'STRICT_JSON': True,
    'COERCE_DECIMAL_TO_STRING': True,
    'UPLOADED_FILES_USE_URL': True,

    # Browseable API
    'HTML_SELECT_CUTOFF': 1000,
    'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...",

    # Schemas
    'SCHEMA_COERCE_PATH_PK': True,
    'SCHEMA_COERCE_METHOD_NAMES': {
        'retrieve': 'read',
        'destroy': 'delete'
    },
}
复制代码
复制代码

很直观的DEFAULTS是一个字典,包含多组键值,key是一个字符串,value是一个元组,我们需要的数据:

'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication''rest_framework.authentication.BasicAuthentication' ), 这有啥用呢,api_settings是怎么使用这个 参数的呢

复制代码
复制代码
class APISettings(object):
    """
    A settings object, that allows API settings to be accessed as properties.
    For example:

        from rest_framework.settings import api_settings
        print(api_settings.DEFAULT_RENDERER_CLASSES)

    Any setting with string import paths will be automatically resolved
    and return the class, rather than the string literal.
    """
    def __init__(self, user_settings=None, defaults=None, import_strings=None):
        if user_settings:
            self._user_settings = self.__check_user_settings(user_settings)
        self.defaults = defaults or DEFAULTS
        self.import_strings = import_strings or IMPORT_STRINGS
        self._cached_attrs = set()
复制代码
复制代码

好像也看不出什么有用的信息,只是一些赋值操作,将DEFAULTS赋给了self.defaults,那我们再回去看,

在视图函数中,我们不定义authentication_classes 就会执行默认的APIView下的authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES,通过这个api_settings对象调用值时,调不到会怎样,我们看看怎么取值吧,首先会在__getattribute__方法中先找,找不到,取自己的属性中找,找不到去本类中找,在找不到就去父类中找,再找不到就去__getattr__中找,最后报错。按照这个逻辑,执行默认 值时最后会走到类中的__getattr__方法中

 

复制代码
复制代码
class APISettings(object):    
    def __getattr__(self, attr):
        if attr not in self.defaults:
            raise AttributeError("Invalid API setting: '%s'" % attr)

        try:
            # Check if present in user settings
            val = self.user_settings[attr]
        except KeyError:
            # Fall back to defaults
            val = self.defaults[attr]

        # Coerce import strings into classes
        if attr in self.import_strings:
            val = perform_import(val, attr)

        # Cache the result
        self._cached_attrs.add(attr)
        setattr(self, attr, val)
        return val
复制代码
复制代码

 

备注:调用__getattr__方法时,会将取的值,赋值给__getattr__中的参数attr,所有,此时的attr是DEFAULT_AUTHENTICATION_CLASSES,这个值在self.defaluts中,所以,代码会执行到try中,val=self.user_settings[attr],这句代码的意思是,val的值是self.user_settings这个东西中取attr,所以self.user_settings一定是个字典,我们看看这个东西做l什么

 

复制代码
复制代码
class APISettings(object):
    @property
    def user_settings(self):
        if not hasattr(self, '_user_settings'):
            self._user_settings = getattr(settings, 'REST_FRAMEWORK', {})
        return self._user_settings
复制代码
复制代码

这个属性方法,返回self._user_settings,这个值从哪里来,看代码逻辑,通过反射,来取settings中的REST_FRAMEWORK的值,取不到,返回一个空字典。那这个settings是哪的,是我们项目的配置文件。

  综合起来,意思就是如果在api_settings对象中找不到这个默认值,就从全局settings中找一个变量REST_FRAMEWORK,这个变量是个字典,从这个字典中找attr(DEFAULT_AUTHENTICATION_CLASSES)的值,找不到,返回一个空字典。而我们的配置文件settings中并没有这个变量,很明显,我们可以在全局中配置这个变量,从而实现一个全局的认证。怎么配?

  配置格式:REST_FRAMEWORK={“DEFAULT_AUTHENTICATION_CLASSES”:("认证代码所在路径","...")}

代码继续放下执行,执行到if attr in self.import_strings:       val = perform_import(val, attr)   这两行就开始,根据取的值去通过字符串的形式去找路径,最后得到我们配置认证的类,在通过setattr(self, attr, val)   实现,调用这个默认值,返回配置类的执行。

 

复制代码
复制代码
def perform_import(val, setting_name):
    """
    If the given setting is a string import notation,
    then perform the necessary import or imports.
    """
    if val is None:
        return None
    elif isinstance(val, six.string_types):
        return import_from_string(val, setting_name)
    elif isinstance(val, (list, tuple)):
        return [import_from_string(item, setting_name) for item in val]
    return val


def import_from_string(val, setting_name):
    """
    Attempt to import a class from a string representation.
    """
    try:
        # Nod to tastypie's use of importlib.
        module_path, class_name = val.rsplit('.', 1)
        module = import_module(module_path)
        return getattr(module, class_name)
    except (ImportError, AttributeError) as e:
        msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
        raise ImportError(msg)
复制代码
复制代码

 

备注:通过importlib模块的import_module找py文件。

 

权限组件

局部视图权限

在app01.service.permissions.py中:

复制代码
复制代码
from rest_framework.permissions import BasePermission
class SVIPPermission(BasePermission):
    message="SVIP才能访问!"
    def has_permission(self, request, view):
        if request.user.user_type==3:
            return True
        return False
复制代码
复制代码

在views.py:

复制代码
复制代码
from app01.service.permissions import *

class BookViewSet(generics.ListCreateAPIView):
    permission_classes = [SVIPPermission,]
    queryset = Book.objects.all()
    serializer_class = BookSerializers
复制代码
复制代码

全局视图权限

settings.py配置如下:

1
2
3
4
REST_FRAMEWORK={
    "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",],
    "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",]
}

权限组件一样的逻辑,同样的配置,要配置权限组件,就要在视图类中定义一个变量permission_calsses = [自定义类]

同样的在这个自定义类中,要定义一个固定的方法   def has_permission(self,request,view)  必须传两个参数,一个request,一个是

当前视图类的对象。权限通过返回True,不通过返回False,我们可以定制错误信息,在自定义类中配置一个静态属性message="错误信息",也可以继承一个类BasePermission。跟认证一样。

复制代码
复制代码
    def check_permissions(self, request):
        """
        Check if the request should be permitted.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request, message=getattr(permission, 'message', None)
                )
复制代码
复制代码

逻辑很简单,循环这个自定义类实例对象列表,从每一个对象中找has_permission,if not  返回值:表示返回False,,就会抛一个异常,这个异常的信息,会先从对象的message属性中找。

  同样的,配置全局的权限,跟认证一样,在settings文件中的REST_FRAMEWORK字典中配一个键值即可。

 

throttle(访问频率)组件

局部视图throttle

在app01.service.throttles.py中:

复制代码
复制代码
复制代码
from rest_framework.throttling import BaseThrottle

VISIT_RECORD={}
class VisitThrottle(BaseThrottle):

    def __init__(self):
        self.history=None

    def allow_request(self,request,view):
        remote_addr = request.META.get('REMOTE_ADDR')
        print(remote_addr)
        import time
        ctime=time.time()

        if remote_addr not in VISIT_RECORD:
            VISIT_RECORD[remote_addr]=[ctime,]
            return True

        history=VISIT_RECORD.get(remote_addr)
        self.history=history

        while history and history[-1]<ctime-60:
            history.pop()

        if len(history)<3:
            history.insert(0,ctime)
            return True
        else:
            return False

    def wait(self):
        import time
        ctime=time.time()
        return 60-(ctime-self.history[-1])
复制代码
复制代码
复制代码

在views.py中:

复制代码
复制代码
from app01.service.throttles import *

class BookViewSet(generics.ListCreateAPIView):
    throttle_classes = [VisitThrottle,]
    queryset = Book.objects.all()
    serializer_class = BookSerializers
复制代码
复制代码

全局视图throttle

REST_FRAMEWORK={
    "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",],
    "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",],
    "DEFAULT_THROTTLE_CLASSES":["app01.service.throttles.VisitThrottle",]
}

同样的,局部使用访问频率限制组件时,也要在视图类中定义一个变量:throttle_classes = [自定义类],同样在自定义类中也要定义一个固定的方法def allow_request(self,request,view),接收两个参数,里面放我们频率限制的逻辑代码,返回True通过,返回False限制,同时要定义一个def wait(self):pass   放限制的逻辑代码。

内置throttle类

在app01.service.throttles.py修改为:

复制代码
复制代码
class VisitThrottle(SimpleRateThrottle):

    scope="visit_rate"
    def get_cache_key(self, request, view):

        return self.get_ident(request)
复制代码
复制代码

settings.py设置:

复制代码
复制代码
REST_FRAMEWORK={
    "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",],
    "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",],
    "DEFAULT_THROTTLE_CLASSES":["app01.service.throttles.VisitThrottle",],
    "DEFAULT_THROTTLE_RATES":{
        "visit_rate":"5/m",
    }
}
复制代码
复制代码

总结

   restframework的三大套件中为我们提供了多个粒度的控制。局部的管控和全局的校验,都可以很灵活的控制。下一个系列中,将会带来restframework中的查漏补缺。

前后端分离之JWT用户认证

 

在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,当下一个request请求时它就把刚刚的资料忘了。于是我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。

传统方式

前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

Json Web Token(JWT)

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

  • 简洁(Compact)

    可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

  • 自包含(Self-contained)

    负载中包含了所有用户所需要的信息,避免了多次查询数据库

JWT 组成

  • Header 头部

头部包含了两部分,token 类型和采用的加密算法

{
  "alg": "HS256",
  "typ": "JWT"
}

它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。

Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

  • Payload 负载

这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。

{
    "iss": "lion1ou JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "lion1ou@163.com"
}

同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

  • Signature 签名

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

  • 签名的目的

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

  • 信息暴露

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

JWT 使用

  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。
  3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

和Session方式存储id的差异

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

  • 单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.comnv.taobao.comnz.taobao.comlogin.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

总结

JWT的主要作用在于

(一)可附带用户信息,后端直接通过JWT获取相关信息。

(二)使用本地保存,通过HTTP Header中的Authorization位提交验证。但其实关于JWT存放到哪里一直有很多讨论,有人说存放到本地存储,有人说存 cookie。个人偏向于放在本地存储,如果你有什么意见和看法欢迎提出。

posted @ 2019-04-13 14:59  nihaoshangjun  阅读(418)  评论(0编辑  收藏  举报