Django-中間件-csrf擴展請求偽造攔截中間件-Django Auth模組使用-效仿 django 中間件配置實現功能插拔式效果-09

  • 2019 年 10 月 3 日
  • 筆記

昨日補充:將自己寫的 login_auth 裝飾裝在 CBV 上

類裡面的方法一般都是類綁定方法 或者 對象綁定方法,第一個參數是類 或者 對象本身,那麼前面寫的裝飾器就要改參數才能用了,不過這裡我們可以用 django 給我們寫好的裝飾器,從不需要更改寫好的裝飾器

三種方式 (別忘了導模組)

from django.utils.decorators import method_decorator    # @method_decorator(login_auth, name='get')  # 第一種, name 參數必須指定  class MyHome(View):      # @method_decorator(login_auth)  # 第二種, get 和 post 都會被裝飾(登錄驗證)(直接把 dispatch 拿過來,加個裝飾器)      def dispatch(self, request, *args, **kwargs):          super().dispatch(request, *args, **kwargs)        @method_decorator(login_auth)  # 第三種,直接裝飾在單個方法上      def get(self, request):          return HttpResponse('get')        def post(self, request):          return HttpResponse('post')

django 中間件

django 中間件 就類似於是 django 的門戶,請求來的時候需要先經過 中間件 才能到達 django 後端(urls),響應走的時候也需要經過 中間件 才能到達 web服務網關介面(wsgif 模組)

django 中間件可以用來做什麼

  • 做網站全局的身份校驗,限制訪問頻率,許可權校驗(反爬)… 只要是涉及到全局的校驗幾乎都可以在中間件中完成,第一時間該想到的也是中間件

django 的中間件是設計比較完善的,邏輯最清晰,最簡單(flask的中間件不如它)

講完這個中間件就知道為什麼我們前面每次提交 post 請求都會寫上先去 settings.py 里把 csrf 這個中間件暫時注釋掉了

django 請求生命周期 *****

經過 中間件 之後才能進入 urls.py(再 views.py … 一層一層遞進)

科普:

  1. wsgiref 不能夠承受高並發,所以上線之後會換成 uwsgi 模組(前面再加一個 nginx 做反向代理)
  • WSGI 與 wsgi 以及 uwsgi 分別什麼意思

​ WSGI是一個協議標準,wsgiref 和 uwsgi 都是實現了 WSGI 協議的功能模組

  1. 請求在進入第一層中間件時會去快取資料庫中判斷有沒有數據
  • 如果有的話會直接拿到數據並返回請求(這樣可以節約資源,降低伺服器以及資料庫的壓力)
  • 如果沒有的話會接著一層一層地走中間件,然後路由配置、views.py …,等請求再次來到最後一層中間件時,在返回數據的同時,會保存一份在快取資料庫中。(下次就可以直接在快取資料庫中拿到數據了)

具體原理等後期涉及到展開來講, 先知道這個概念就行

默認中間件及其大概方法組成

django 默認有七個中間件

django 支援用戶自定義自己的中間件,並且暴露給用戶,還暴露給用戶五個可以自定義中間件的方法

# settings.py 里的七個默認中間件  MIDDLEWARE = [      'django.middleware.security.SecurityMiddleware',      'django.contrib.sessions.middleware.SessionMiddleware',      'django.middleware.common.CommonMiddleware',      'django.middleware.csrf.CsrfViewMiddleware',      'django.contrib.auth.middleware.AuthenticationMiddleware',      'django.contrib.messages.middleware.MessageMiddleware',      'django.middleware.clickjacking.XFrameOptionsMiddleware',  ]  

點進去觀察 中間件源碼

'''  django.middleware.security.SecurityMiddleware  --> 本質是動態導入(可以看最後面的那個模仿案例)  # from django.middleware.security import SecurityMiddleware    django.middleware.csrf.CsrfViewMiddleware  --> from django.middleware.csrf import CsrfViewMiddleware  '''

發現 django 中間件 中有五個用戶可以自定義的方法

# django.middleware.csrf.CsrfViewMiddleware  --> from django.middleware.csrf import CsrfViewMiddleware  class CsrfViewMiddleware(MiddlewareMixin):      def _accept(self, request):      def _reject(self, request, reason):      def _get_token(self, request):      def _set_token(self, request, response):      def process_request(self, request):      def process_view(self, request, callback, callback_args, callback_kwargs):      def process_response(self, request, response):    # django.middleware.security.SecurityMiddleware  --> django.middleware.security.SecurityMiddleware  class SecurityMiddleware(MiddlewareMixin):      def __init__(self, get_response=None):      def process_request(self, request):      def process_response(self, request, response):    # django.contrib.sessions.middleware.SessionMiddleware  class SessionMiddleware(MiddlewareMixin):      def __init__(self, get_response=None):      def process_request(self, request):      def process_response(self, request, response):  
  • 需要我們掌握的方法有
  1. process_ request() 方法
  2. process_ response ()方法
  • 需要了解的方法
  1. process_ view()
  2. process exception ()
  3. process_ template_ response ()

中間件的執行順序

大體同 django 請求生命周期 那張圖,可能會受以下情況的影響

自定義中間件探究不同操作對中間件執行順序的影響

測試思路:

  • 在 settings.py 里註冊不同中間件,探究默認的執行順序
  • 在不同中間件的 process_request 和 process_response 等方法中 return HttpResponse 對象會對執行順序造成什麼影響
  • 了解五種方法的觸發時機

自定義中間件

  1. 新建一個文件夾(放在全局或 app 內)
  2. 寫一個類繼承 MiddlewareMiXin 類
  3. 裡面書寫需要的(五個方法中的某些)方法
  4. 一定要在 settings.py 里配置中間件

程式碼

mymiddleware/mdd.py 自定義中間件

from django.utils.deprecation import MiddlewareMixin  from django.shortcuts import HttpResponse      class MyMdd(MiddlewareMixin):      def process_request(self, request):          print('我是第一個中間件裡面的process_request方法')        def process_response(self, request, response):          print('我是第一個中間件裡面的process_response方法')          return response        def process_view(self, request, view_func, view_args, view_kwargs):          print(view_func)          print(view_args)          print(view_kwargs)          print('我是第一個中間件裡面的process_view方法')        def process_exception(self, request, exception):          print('我是第一個中間件裡面的process_exception')        def process_template_response(self, request, response):          print('我是第一個中間件裡面的process_template_response')          return response      class MyMdd1(MiddlewareMixin):      def process_request(self, request):          print('我是第二個中間件裡面的process_request方法')        def process_response(self, request, response):          print('我是第二個中間件裡面的process_response方法')          return response        def process_view(self, request, view_func, view_args, view_kwargs):          print(view_func)          print(view_args)          print(view_kwargs)          print('我是第二個中間件裡面的process_view方法')        def process_exception(self, request, exception):          print('我是第二個中間件裡面的process_exception')        def process_template_response(self, request, response):          print('我是第二個中間件裡面的process_template_response')          return response      class MyMdd2(MiddlewareMixin):      def process_request(self, request):          print('我是第三個中間件裡面的process_request方法')        def process_response(self, request, response):          print('我是第三個中間件裡面的process_response方法')          return response        def process_view(self, request, view_func, view_args, view_kwargs):          print(view_func)          print(view_args)          print(view_kwargs)          print('我是第三個中間件裡面的process_view方法')        def process_exception(self, request, exception):          print('我是第三個中間件裡面的process_exception')        def process_template_response(self, request, response):          print('我是第三個中間件裡面的process_template_response')          return response  

在 settings.py 中的配置

MIDDLEWARE = [      'django.middleware.security.SecurityMiddleware',      'django.contrib.sessions.middleware.SessionMiddleware',      'django.middleware.common.CommonMiddleware',      'django.middleware.csrf.CsrfViewMiddleware',      'django.contrib.auth.middleware.AuthenticationMiddleware',      'django.contrib.messages.middleware.MessageMiddleware',      'django.middleware.clickjacking.XFrameOptionsMiddleware',      'mymiddleware.mdd.MyMdd',  # 配置上      'mymiddleware.mdd.MyMdd1',  # 配置上      'mymiddleware.mdd.MyMdd2',  # 配置上  ]

需要掌握的方法

process_request

請求來的時候會依次執行 settings.py 配置文件中註冊了的中間件里的該方法

  • 如果沒有該方法則直接跳過,走下一個中間件
  • 如果該方法里返回了 HttpResponse 對象,那麼會直接從當前中間件的 process_response 方法 從下往上依次執行返回,不會再接著往下執行
  • 執行順序:從上往下
  • 該方法可以實現對用戶身份的校驗,訪問頻率的限制,用戶許可權的校驗…

基於該特點就可以做訪問頻率限制

process_response

響應走的時候會依次執行 settings.py 配置文件中註冊了的中間件里的該方法(必須將 response 形參返回,因為這個 response 指代的就是返回給前端的數據)

  • 如果沒有該方法則直接跳過,走下一個中間件
  • 執行順序:從下往上
  • 該方法可以幫你實現快取機制(減緩伺服器、資料庫的壓力)

需要了解的方法

process_view

路由匹配成功 執行視圖函數之前 自動觸發(從上往下依次執行)

process_exception

視圖函數報錯了,自動觸發(從下往上依次執行)

process_template_response

視圖函數返回的 HttpResponse 對象中包含了 render 屬性時會觸發,或者是表明一個對象時 TemplateResponse 對象或等價方法 的時候也會觸發(從下往上依次執行)

def index(request):      print("我是 index 視圖函數")      def render():          return HttpRespone('用戶最終能夠看到的結果')  # ******      obj = HttpResponse('index')      obj.render = render  # 返回的 HttpResponse 對象中必須包含 render 屬性,才能觸發中間件里定義的 process_template_response 方法      return obj

強調:

在寫中間件的時候,只要形參中有 response ,就要記得將其返回,這個Response 是要給前端的資訊

csrf 中間件 跨站請求偽造

釣魚網站

原理:寫了一個一模一樣的網站,一個隱藏框,發送往隱藏當做收錢方

問題:如何區分當前用戶朝我們網站發送的請求頁面是不是我們本網站給的

防止思路

網站會給返回給用戶的 form 表單頁面 偷偷塞一個隨機字元串

請求到來的時候,會先比對隨機字元串是否一致,如果不一致,直接拒絕(403 FORBIDDEN)

解決方案

在頁面上放一個 隱藏的 input 框,value 裡面放的是一個字元串,每次刷新都會更新裡面的 value,這樣別人的網站就不知道;,這個 value 就無法偽造了

django 的實現 {% csrf_token %}

該隨機字元串有以下特點:

  • 同一個瀏覽器每一次訪問都不一樣
  • 不同瀏覽器絕對不一樣

post請求提交數據通過 csrf 校驗

form 表單

form 表單發送 post 請求的時候,需要你做的是寫一段程式碼 {% csrf_token %} 即可,不需要注釋 csrf 中間件了

ajax 發送

三種方式(第三種可以用在前後端分離時)

  1. 先在頁面上寫 {% csrf_token %},利用標籤查找,獲取到該 input 鍵值資訊,放到 data 里

  2. ajax data 值 那裡直接寫 {{ csrf_token }}data:{'username':'jason','csrfmiddlewaretoken':'{{ csrf_token }}'},

  3. 參考官方文檔推薦,自定義 js 文件,在要用到的頁面載入這個 js 腳本,自動獲取並傳遞 csrf 校驗 *****

    • 你可以將下面的 js 程式碼 放到一個 js 文件中
    // js 程式碼(一般放在 static 文件夾下)  function getCookie(name) {      var cookieValue = null;      if (document.cookie && document.cookie !== '') {          var cookies = document.cookie.split(';');          for (var i = 0; i < cookies.length; i++) {              var cookie = jQuery.trim(cookies[i]);              // Does this cookie string begin with the name we want?              if (cookie.substring(0, name.length + 1) === (name + '=')) {                  cookieValue = decodeURIComponent(cookie.substring(name.length + 1));                  break;              }          }      }      return cookieValue;  }    var csrftoken = getCookie('csrftoken');      function csrfSafeMethod(method) {      // these HTTP methods do not require CSRF protection      return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));  }    $.ajaxSetup({      beforeSend: function (xhr, settings) {          if (!csrfSafeMethod(settings.type) && !this.crossDomain) {              xhr.setRequestHeader("X-CSRFToken", csrftoken);          }      }  });
    • 之後要用到的的地方 <script src="{% static 'setjs.js' %}"></script> 導入就行
    • 這樣就不需要 在 html 中寫 {% csrf_token %} 或在 ajax 中寫 {{ csrf_token }}

csrf 裝飾器相關

其他中間件也可以效仿下面的方法來校驗或者取消校驗

兩個問題

當你網站全局都需要校驗 csrf 的時候(未注釋掉 csrf 中間件時),有幾個不需要校驗該如何處理? @csrf_exempt

當你的網站全局不校驗 csrf 的時候(注釋掉 csrf 中間件時),有幾個需要校驗該如何處理 ?@csrf_protect

未注釋掉 csrf 中間件時 單功能取消 csrf 校驗:csrf_exempt

FBV

from django.views.decorators.csrf import csrf_exempt    # 全局開啟時,局部禁用  @csrf_exempt  def index(request):    pass

CBV

有兩種方式,不能針對單個方法,是針對全局的

# CBV比較特殊,不能單獨加在某個方法上  # 只能加在類上或dispatch方法上  from django.utils.decorators import method_decorator  from django.views.decorators.csrf import csrf_exempt      # @method_decorator(csrf_exempt,name='dispatch')  # 第一種  class Csrf_Token(View):    @method_decorator(csrf_exempt)  # 第二種    def dispatch(self,request,*args,**kwargs):      res = super().dispatch(request,*args,**kwargs)      return res    # @method_decorator(csrf_exempt)  # 這裡這麼寫不行!!!    def get(self,request):      pass    def post(self,request):      pass

注釋掉 csrf 中間件時 單功能開啟 csrf 校驗:csrf_protect

FBV

from django.views.decorators.csrf import csrf_protect      @csrf_protect  def lll(request):      return HttpResponse('lll')  

CBV 還要改改

有三種方式,既可以針對全局的,也可以針對單個的

from django.views.decorators.csrf import csrf_protect    from django.views import View  from django.utils.decorators import method_decorator      # 第一種方式  # @method_decorator(csrf_protect,name='post')  # 有效的  class MyView(View):      @method_decorator(csrf_protect)  # 第三種方式      def dispatch(self, request, *args, **kwargs):          res = super().dispatch(request, *args, **kwargs)          return res        def get(self, request):          return HttpResponse('get')        # 第二種方式      # @method_decorator(csrf_protect)  # 有效的      def post(self, request):          return HttpResponse('post')  

總結:csrf 裝飾器中只有 csrf_exempt 是特例,其他的裝飾器在給CBV 裝飾的時候 都可以有三種方式

Auth 模組

科普提示小點:

  • 一個方法的放回結果列印出來是 「字元串」 最好 type 確認一下,可能是對象重寫了 __str__() 方法
  • django 後台管理只有超級用戶才能進入
  • 用了 Auth 模組的方法 ,就最好都用 Auth 模組的方法
  • 修改密碼必須調用 .save() 保存,否則無效

使用 django 自帶的 auth 表做登錄功能

涉及到的 auth 相關方法

python3 manage.py createsuperuser  # 命令行下創建超級用戶(可以擁有登錄 django admin 後台管理的許可權)    # 查詢用戶是否存在  user_obj = auth.authenticate(username=username, password=password)  # 資料庫中的密碼是密文的(該方法不能只傳用戶名一個參數),返回值要麼是對象,要麼是 None    # 記錄用戶狀態  auth.login(request, user_obj)  # 登錄,會自動存 session  # 優點:只要執行了這一句話,你就可以在後端任意位置通過 request.user 拿到當前登錄的用戶對象(未登錄會報錯,AnonymousUser 匿名用戶)    # 獲取用戶對象  request.user  # 用戶登錄了直接獲取用戶對象,用戶沒登錄獲取到 AnonymousUser 匿名用戶    # 判斷當前用戶是否登錄,未登錄(AnonymousUser)會返回 False,其他情況下返回 True  request.user.is_authenticated    # 驗證用戶密碼是否正確  is_right = request.user.check_password(old_password)  # 將獲取的用戶密碼,自動加密,然後去資料庫中對比(返回布爾值)    # 修改密碼  request.user.set_password(new_password)  # 修改密碼  request.user.save()  # 需要保存才能生效    # 註銷用戶  auth.logout(request)  # 等價於 request.session.flush() (刪除了 session  表中記錄,瀏覽器 cookie)      # 登錄驗證裝飾器  from django.contrib.auth.decorators import login_required    # @login_required  # 自動校驗當前用戶是否登錄,如果沒有登錄,(未傳參數的情況下)默認跳轉到 django 自帶的登錄頁面(還是 404 ?)  # ------ 局部配置  @login_required(login_url='/login/')  def set_password(request):      pass    # ------ 全局配置(不用在裡面寫配置了)  # 在 settings.py 中寫  LOGIN_URL = '/login/'      # 註冊用戶  from django.contrib.auth.models import User  # 這就是那張 auth 表  # 創建普通用戶  User.objects.create_user(username=username, password=password)  # 創建超級用戶  User.objects.create_superuser(username=username, password=password, email='[email protected]')  # 創建超級用戶必須傳郵箱  # 不能用 User.objects.create(username=username, password=password)  (這樣密碼沒有加密)  

核心程式碼

app01/views.py

from django.shortcuts import render, HttpResponse  from django.contrib import auth      def xxx(request):      if request.method == 'POST':          username = request.POST.get('username')          password = request.POST.get('password')          # 取資料庫查詢當前用戶數據          # models.User.objects.filter(username=username,password=password).first()          user_obj = auth.authenticate(username=username, password=password)  # 必須要用 因為資料庫中的密碼欄位是密文的 而你獲取的用戶輸入的是明文          print(user_obj)          # print(user_obj)          # print(user_obj.username)          # print(user_obj.password)          # 保存用戶狀態          # request.session['user'] = user_obj          auth.login(request, user_obj)  # 將用戶狀態記錄到session中          """只要執行了這一句話  你就可以在後端任意位置通過request.user獲取到當前用戶對象"""      return render(request, 'xxx.html')      def yyy(request):      print(request.user)  # 如果沒有執行auth.login那麼拿到的是匿名用戶      print(request.user.is_authenticated)  # 判斷用戶是否登錄  如果是你們用戶會返回False      # print(request.user.username)      # print(request.user.password)      return HttpResponse('yyy')      from django.contrib.auth.decorators import login_required      # 修改用戶密碼  @login_required  # 自動校驗當前用戶是否登錄  如果沒有登錄 默認跳轉到 一個莫名其妙的登陸頁面  def set_password(request):      if request.method == 'POST':          old_password = request.POST.get('old_password')          new_password = request.POST.get('new_password')          # 先判斷原密碼是否正確          is_right = request.user.check_password(old_password)  # 將獲取的用戶密碼 自動加密 然後去資料庫中對比當前用戶的密碼是否一致          if is_right:              print(is_right)              # 修改密碼              request.user.set_password(new_password)              request.user.save()  # 修改密碼的時候 一定要save保存 否則無法生效      return render(request, 'set_password.html')      @login_required  def logout(request):      # request.session.flush()      auth.logout(request)      return HttpResponse("logout")      from django.contrib.auth.models import User      def register(request):      if request.method == 'POST':          username = request.POST.get('username')          password = request.POST.get('password')          user_obj = User.objects.filter(username=username)          if not user_obj:              # User.objects.create(username =username,password=password)  # 創建用戶名的時候 千萬不要再使用create 了              # User.objects.create_user(username =username,password=password)  # 創建普通用戶              User.objects.create_superuser(username=username, password=password, email='[email protected]')  # 創建超級用戶      return render(request, 'register.html')  

自定義擴展 autor 表欄位

前提:

settings.py 添加額外配置

# ... 其他配置  # 告訴 django 不再使用 auth 默認的表  而是使用你自定義的表  AUTH_USER_MODEL = 'app01.Userinfo'  # '應用名.模型表類名'  # ... 其他配置

兩種方式

app01/models.py

from django.db import models  from django.contrib.auth.models import AbstractUser      # Create your models here.  # 第一種 使用一對一關係  不考慮      # 第二種方式   使用類的繼承  class Userinfo(AbstractUser):      # 千萬不要跟原來表(AbstractUser)中的欄位有衝突      phone = models.BigIntegerField()      avatar = models.CharField(max_length=32)      # 別忘了去 settings.py 里配置

後續操作

執行資料庫遷移命令(python3 manage.py makemigrations、python3 manage.py migrate

這樣以後,所有的 auth 模組功能,全部都基於你創建的表,而不再使用 auth_user(不會再自動創那些表了)

效仿 django中間件配置 實現 功能插拔式效果

django 的中間件 其實就是一個類,一個個功能可以寫成類,注釋掉就不執行了

我們效仿中間件(後面要學的 restframework 的設計思想也是這樣的),做一個通知功能, 可以發微信通知、簡訊通知、右鍵通知

程式碼實現

重點配置部分(設計思想好好學學)

start.py 入口文件

import notify    notify.send_all('國慶放假了 記住放八天哦')  

notify/__init__.py 關鍵程式碼(結合了 importlib 動態導入、反射 等知識點)

import settings  import importlib      def send_all(content):      for path_str in settings.NOTIFY_LIST:  # 1.拿出一個個的字元串   'notify.email.Email'          module_path, class_name = path_str.rsplit('.', maxsplit=1)  # 2.從右邊開始 按照點切一個 ['notify.email', 'Email']          module = importlib.import_module(module_path)  # from notity import msg/email/wechat          cls = getattr(module, class_name)  # 利用反射 一切皆對象的思想 從文件中獲取屬性或者方法 cls = 一個個的類名          obj = cls()  # 類實例化生成對象          obj.send(content)  # 對象調方法  

settings.py 配置(可以在這裡開啟或關閉功能)

NOTIFY_LIST = [      'notify.email.Email',      'notify.msg.Msg',      # 'notify.wechat.WeChat',  # 注釋掉了,這個功能就不執行了      'notify.qq.QQ',  ]  

功能擴展部分

然後是各個功能的文件(拆分成了各個文件,搭配 settings.py 起到可插拔式效果),要想新增這種功能直接加個文件實現這幾個程式碼即可

notify/email.py

class Email(object):      def __init__(self):          pass  # 發送郵件需要的程式碼配置        def send(self, content):          print('郵件通知:%s' % content)  

notify/msg.py

class Msg(object):      def __init__(self):          pass  # 發送簡訊需要的程式碼配置        def send(self, content):          print('簡訊通知:%s' % content)  

notify/qq.py

class QQ(object):      def __init__(self):          pass  # 發送qq需要的程式碼準備        def send(self, content):          print('qq通知:%s' % content)  

notify/wechat.py

class WeChat(object):      def __init__(self):          pass  # 發送微信需要的程式碼配置        def send(self, content):          print('微信通知:%s' % content)  
補充:pycharm 使用技巧

想知道當前跳進來的程式碼的位置,可以點擊圖中的圖標,快速定位,可以讓你知道目錄結構(看看推測對不對)