1. 程式人生 > 其它 >簡單的Django auth + React 跨域cookie登入

簡單的Django auth + React 跨域cookie登入

把原來的程式碼,用React後端分離來重構,踩了挺多坑的,特別是這個登入功能,坑太多了。

登入介面

首先是後端的登入介面,這個簡單,用的是django自帶的auth庫。遷移資料庫的時候,會自動生成一張使用者表,可以通過admin後臺來管理使用者,這是django的基礎,就不多說了。

def login_react(request):
    from django.contrib import auth
    data = json.loads(request.body)
    username = data.get('username', '')
    password = data.get('password', '')
    user_object = auth.authenticate(username=username, password=password)
    if user_object:
        # 登入成功為止
        auth.login(request, user_object)
        request.session['user'] = username
        return JsonResponse({'code': 0, 'msg': '登入成功'})
    return JsonResponse({'code': -1, 'msg': '使用者名稱或密碼錯誤'})

跨域解決

跨域一般後端解決,解決起來也簡單,兩種方式。

一、自定義中介軟體

第一種是在每個請求的響應頭加上跨域欄位,可以手寫一箇中間件,類似這種:

from django.utils.deprecation import MiddlewareMixin


class HttpResponseCustomHeader(MiddlewareMixin):
    # 全域性修改響應header
    def process_response(self, request, response):
        response['Access-Control-Allow-Origin'] = '*'
        if request.method == 'OPTIONS':
            response["Access-Control-Allow-Headers"] = "Content-Type"
            response["Access-Control-Allow-Methods"] = "PUT,PATCH,DELETE"
        return response

二、django-cors-headers第三方庫

第二種可以用一個第三方庫:

pip install django-cors-headers

後面配置一下settings.py檔案,網上很多教程,一搜一大把。


axios登入請求

大概就是這樣:

$axios.post(
  'http://127.0.0.1/login/',
  {
    username: xxxx,
    password: xxxx,
  },
).then((res) => {
  if (res.data.code === 0) {
    // 跳轉到首頁
  } else {
    // 登入失敗的處理
  }
})

坑一、set-cookie黃色感嘆號

第一個坑出現了,用Django自帶的auth登入成功,返回的response header會自動寫入一個set-cookie欄位,瀏覽器接到響應之後,會將set-cookie中的值寫入瀏覽器,這是cookie登入的原理。

抓包看一下,登入成功之後,響應頭裡明明有set-cookie欄位,但是cookie卻沒有寫入,提示SameSite=Lax時,set-cookie行為被禁止了。

然後百度,中間踩了許多坑:

1、後端返回時,將SameSite欄位設定為None,這個做法不成功。直接配置corsheaders中介軟體,將SESSION_COOKIE_SAMESITE = None CSRF_COOKIE_SAMESITE = None ,不生效,如果設定成'None',後端會報錯,顯示:SameSite欄位只能是Lax或者Strict;

2、手寫中介軟體,篡改響應頭的SameSite欄位,這個改成功了,返回的set-cookie欄位裡面確實沒有SameSite欄位了,但是還是標黃色感嘆號,沒寫入成功;

3、瀏覽器輸入chrome://flags/,將SameSite by default cookies 和 Cookies without SameSite must be secure設定為disable,實測失敗;

正確的解決方式,有兩種:

1、axios請求時,把127.0.0.1,替換成localhost;

2、使用corsheaders中介軟體,在settings.py中加入這兩個欄位:SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = 'None',同時登入介面使用https請求。


坑二、set-cookie仍無法寫入

我在本地用的是第一種方式,直接將127.0.0.1,替換成localhost,但是這樣還是不行,因為axios請求預設是不寫入、不攜帶cookie的,需要在請求中加入一個欄位withCredentials: true,因為之後所有的請求都需要攜帶cookie,所以這裡要封裝一下axios:

import axios from 'axios'

const service = axios.create({
  baseURL: 'http://localhost:8000',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json; charset=UTF-8;',
  },
  withCredentials: true
})
export default service

在登入元件中呼叫:

import $axios from './request/index'

$axios.post(
  '/login/',
  {
    username: values.username,
    password: values.password,
  }
).then((res) => {
  if (res.data.code === 0) {
    // 跳轉到首頁
  } else {
    // 登入失敗的處理
  }
})

大功告成,可以看到set-cookie後面的黃色感嘆號消失,sessionId也成功寫入:


其他攜帶cookie的請求

這個就比較簡單了,後端在接到請求之後,如果校驗cookie不正確,就給重定向到一個介面,這個介面返回一個登入不成功的資訊,然後axios接受到這個資訊,就給重定向到登入頁。

一、後端處理

新開一個介面:no_login

def no_login(request):
    return JsonResponse({'code': -1, 'msg': '未登入'})

在需要登入的介面上加一個裝飾器:@login_required(login_url='/no_login'),如:

@login_required(login_url='/no_login')
def login_out(request):
    from django.contrib import auth
    auth.logout(request)
    return JsonResponse({'code': 0, 'msg': '退出登入成功'})

這樣,如果前端請求login_out介面時,django會自動校驗cookie,如果校驗失敗,就重定向到no_login介面,由no_login介面返回錯誤資訊給前端

另:上面示例的是退出登入介面,在執行完auth.logout(request)後的響應,django會自動構造響應頭,將set-cookie欄位中的sessionId設為空,這樣瀏覽器接到請求後,就會將sessionId從網站的cookie中移除了。


二、前端處理

前端不可能每次發請求都判斷一下後端的msg是不是“未登入”,所以還得去封裝axios:

import axios from 'axios'

const service = axios.create({
  baseURL: 'http://localhost:8000',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json; charset=UTF-8;',
  },
  withCredentials: true
})

// 新增對響應的處理
service.interceptors.response.use((response) => {
  if (response.data.msg === '未登入') {
    window.location.href = '/login/'
  }else {
    return response
  }
})

export default service

這樣就ok了。


個人踩坑過來的,不知道是否是最佳實踐,酌情使用哈,有錯誤之處歡迎大家批評指正。