drf相關問題

drf自定義用戶認證:html

登陸默認 使用django的ModelBackend,對用戶名和密碼進行驗證。但咱們平時登陸網站時除了用戶名也能夠用郵箱或手機進行登陸,這就須要咱們本身擴展backend前端

1、settings中配置python

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

users.views中新建backend,對用戶名或手機進行驗證git

2、users/views.pygithub

# 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有效時間設置web

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的區別:django

  • 保存在數據庫中,若是是一個分佈式的系統,就很是麻煩
  • token永久有效,沒有過時時間。

 

Django Rest framework實現流程

目錄

一 什麼是restful架構
二 Django REST framework簡介
三 Django REST framework原理
四 Django REST framework源碼流程
五 Django REST framework實現用戶登陸json

一 什麼是restful架構

一、起源

REST即表述性狀態傳遞(英文:Representational State Transfer,簡稱REST)是Roy Fielding博士在2000年他的博士論文中提出來的一種軟件架構風格。它是一種針對網絡應用的設計和開發方式,能夠下降開發的複雜性,提升系統的可伸縮性。
目前在三種主流的Web服務實現方案中,由於REST模式的Web服務與複雜的SOAP和XML-RPC對比來說明顯的更加簡潔,愈來愈多的web服務開始採用REST風格設計和實現。例如,Amazon.com提供接近REST風格的Web服務進行圖書查找;雅虎提供的Web服務也是REST風格的

二、框架組成

根據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):從服務器刪除資源。

三、認證機制

咱們知道,客戶端經過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方法。這個方法主要用來向客戶端返回還須要多少時間能夠繼續訪問的提示。

     

    組件2、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。

     

組件3、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。我的偏向於放在本地存儲,若是你有什麼意見和見解歡迎提出。

相關文章
相關標籤/搜索