从 Vue.js 向 Django 发送 POST/DELETE 请求时禁止未设置 CSRF cookie

问题描述

我一直在尝试将 POST 或 DELETE 请求从我的 Vue 前端发送到我的 Django 后端。

我在 localhost:3000 上运行 Vue.js,在 localhost:8000 上运行 Django。 我已经使用 django-cors-headers 设置了 CORS,并且我能够 GET 请求。但是,一旦我尝试删除或发布,我就会收到此错误

Forbidden (CSRF cookie not set.)

我明白,我需要在我的请求标头中传递一个 CSRF 令牌,我有

    deleteImage() {
      const url = this.serverURL + 'images/delete/' + this.image_data.pk;

      const options = {
        method: "DELETE",headers: {'X-CSrftoken': this.CSrftoken}
      };
      fetch(url,options)
        .then(response => {
          console.log(response);
          if (response.ok){ 
            // if response is successful,do something
          }
        })
        .catch(error => console.error(error));
    }

我从 GET 请求中获取 this.CSrftoken ,如果我使用 Django docs 中所示的方法,令牌是相同的。

我的 Django settings.py 看起来像这样:

rom pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Security WARNING: keep the secret key used in production secret!
SECRET_KEY = '***'

# Security WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application deFinition

INSTALLED_APPS = [
    'django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','corsheaders','serveImages.apps.ServeimagesConfig','django_admin_listfilter_dropdown',]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware','corsheaders.middleware.CorsMiddleware','django.middleware.common.CommonMiddleware',]

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000","http://127.0.0.1:3000"
]


CSRF_TRUSTED_ORIGINS = [
    "http://localhost:3000","http://127.0.0.1:3000"
]

而且我知道认情况下 django-cors-headers 允许标题 X-CSrftoken

我已经在 StackOverflow 上完成了之前关于这个主题的所有问题,但似乎没有任何效果

更多背景: views.py

from django.http import JsonResponse
import os
from django.conf import settings
from django.middleware import csrf

from .models import Image


def get_csrf_token(request):
    token = csrf.get_token(request)
    return token
    # return JsonResponse({'CSrftoken': token})

def index(request,dataset,class_label):
    payload = {}

    images_folder_url = os.path.join('static','images',class_label.lower())
    payload['base_url'] = images_folder_url

    data_query = Image.objects.filter(dataset__name=dataset,label__name=class_label).values('pk','path','isIncluded')
    payload['image_data'] = list(data_query)
    payload['number_of_images'] = len(payload['image_data'])
    payload['CSrftoken'] = get_csrf_token(request)

    return JsonResponse(payload)

def delete_image(request,img_pk):
    print(request)
    # Just for testing
    return JsonResponse({'status': '200'})

urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('get-token',views.get_csrf_token,name='CSrftoken'),path('images/<str:dataset>/<str:class_label>',views.index,name='index'),path('images/delete/<int:img_pk>',views.delete_image,name='delete_image'),]

解决方法

好的,所以我以前经历过这场战斗,至少可以说令人沮丧。如果我完全诚实,那是因为我不了解所涉及的所有设置的推动力或相互作用。由于没有时间阅读所有文档,因此仍然不要阅读其中的一些内容。 无论如何,有很多潜在的问题,我将介绍一些我遇到的您可能会在这里处理的问题。

首先,确保您通过相同的 URL 运行 Vue.js 应用程序。如在,如果您在 127.0.0.1:8080 上运行 django,那么您的 Vue 应用程序应该在 127.0.0.1:3000 而不是本地主机上运行。这可能不是你当前的问题,但它可以给你一个wtf时刻。如果您的最终设置是从与后端不同的域为前端提供服务,那么您可能需要调整一些设置。

接下来,启用 CORS 以允许 cookie 包含在跨站点 http 请求中。这可能不是您当前的问题,但接下来会困扰您。

# https://github.com/adamchainz/django-cors-headers#cors_allow_credentials
CORS_ALLOW_CREDENTIALS = True

最后,要真正解决您当前的问题,首先我会改用 axios 来处理您在前端的请求。

import axios from 'axios'

axios.defaults.xsrfHeaderName = 'x-csrftoken'
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.withCredentials = true

let djangoURL = 'http://127.0.0.1:8000'
// `timeout` specifies the number of milliseconds before the request times out.
// Because we enable Django Debug Toolbar for local development,there is often
// a processing hit. This can also be tremendously bad with unoptimized queries.
let defaultTimeout = 30000
if (process.env.PROD) {
  djangoURL = 'https://##.##.#.##:###'
  defaultTimeout = 10000
}
axios.defaults.baseURL = djangoURL
axios.defaults.timeout = defaultTimeout

const api = axios.create()
export { api }

defaultTimeout 设置和条件本地 vs prod 评估是完全可选的。拥有它真是太好了。 发送您的请求应该是这样的:

new Promise((resolve,reject) => {
    api.delete('images/delete/' + this.image_data.pk).then(response => {
      // console.log(response)
      resolve(response)
    },error => {
      // console.log(error)
      reject(error)
    })
  })

最后,将 http only cookie 设置为 false

# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
CSRF_COOKIE_HTTPONLY = False

有时对我有帮助的一个很好的参考示例是这个博客: https://briancaffey.github.io/2021/01/01/session-authentication-with-django-django-rest-framework-and-nuxt 和这个博客: https://testdriven.io/blog/django-spa-auth/ 第二个设置了一些不适用的设置,它正在反应中,但视图的设置仍然是一个好的开始。 您最终会想要合并身份验证,所以现在就选择;会话或 jwt。我选择了基于会话的身份验证,但有些人在想要通过第三方(如 0auth 等)进行身份验证时使用 jwt。

我用于身份验证的视图示例:

import json
# import logging

from django.contrib.auth import authenticate,login,logout
from django.http import JsonResponse
from django.middleware.csrf import get_token
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST
from rest_framework.authentication import SessionAuthentication,BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

class SessionView(APIView):
    authentication_classes = [SessionAuthentication,BasicAuthentication]
    permission_classes = [IsAuthenticated]

    @staticmethod
    def get(request,format=None):
        return JsonResponse({'isAuthenticated': True})


class WhoAmIView(APIView):
    authentication_classes = [SessionAuthentication,format=None):
        return JsonResponse({'username': request.user.username})


@ensure_csrf_cookie
def get_csrf(request):
    response = JsonResponse({'detail': 'CSRF cookie set'})
    response['X-CSRFToken'] = get_token(request)
    return response


@require_POST
def login_view(request):
    data = json.loads(request.body)
    username = data.get('username')
    password = data.get('password')

    if username is None or password is None:
        return JsonResponse({'detail': 'Please provide username and password.'},status=400)

    user = authenticate(username=username,password=password)

    if user is None:
        return JsonResponse({'detail': 'Invalid credentials.'},status=400)

    login(request,user)
    return JsonResponse({'detail': 'Successfully logged in.'})


def logout_view(request):
    if not request.user.is_authenticated:
        return JsonResponse({'detail': 'You\'re not logged in.'},status=400)

    logout(request)
    return JsonResponse({'detail': 'Successfully logged out.'})

urls.py

urlpatterns = [
    path('csrf/',views.get_csrf,name='api-csrf'),path('login/',views.login_view,name='api-login'),path('logout/',views.logout_view,name='api-logout'),path('session/',views.SessionView.as_view(),name='api-session'),# new
    path('whoami/',views.WhoAmIView.as_view(),name='api-whoami'),# new
]