石墨烯 django 端点是否同时需要 X-Csrftoken 和 CsrfCookie?

Does a graphene django Endpoint expects a X-Csrftoken and CsrfCookie at the same time?

使用:


我正在使用 Django、GraphQL 和 Vue-Apollo 测试单页 Vue 应用程序。

如果我在我的视图中使用 csrf_exempt,一切都在前端工作。

urlpatterns = [
<...>
   path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
<...>

现在我想用 CSRF 保护我的请求。 在了解 CSRF 保护的过程中,我认为所有 Django GraphQLView 需要的是在请求 Header 中接收 X-Csrftoken 的 "value"。所以我专注于以不同的方式发送 csrf 价值......通过像这样的单一视图

path('csrf/', views.csrf),
path("graphql", GraphQLView.as_view(graphiql=True)),

或通过 ensure_csrf_cookie

确保 cookie

之后,在我的 ApolloClient 中,我获取了这些值并将请求 Header 发回给他。

这是我从 Django-Vue 页面发送 GraphQL 请求时 Django 打印的内容。

Forbidden (CSRF token missing or incorrect.): /graphql

Parallel 我总是用 graphiql IDE 测试,这些请求仍然有效。我还每次打印查询解析器的 info.context.headers 值。

{'Content-Length': '400', 'Content-Type': 'application/json',
'Host': 'localhost:7000', 'Connection': 'keep-alive',
'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 
'Accept': 'application/json', 'Sec-Fetch-Dest': 'empty', 'X-Csrftoken': 'dvMXuYfAXowxRGtwSVYQmpNcpGrLSR7RuUnc4IbIarjljxACtaozy3Jgp3YOkMGz',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36',
'Origin': 'http://localhost:7000',
'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors',
'Referer': 'http://localhost:7000/graphql', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'en-US,en;q=0.9,de;q=0.8',
'Cookie': 'sessionid=jqjvjfvg4sjmp7nkeunebqos8c7onhiz; csrftoken=dvMXuYfAXowxRGtwSVYQmpNcpGrLSR7RuUnc4IbIarjljxACtaozy3Jgp3YOkMGz'}

我认识到 GraphQLView IDE 总是将 X-CsrftokenCookie:..csrftoken. 也放在请求中。如果在发送请求之前删除 GraphQLView IDE 的 csrftoken-cookie,我会得到这个

Forbidden (CSRF cookie not set.): /graphql

IDE 显示一个长长的红色报告

.... CSRF verification failed. Request aborted.</p>\n\n\n  
<p>You are seeing this message because this site requires a CSRF cookie when submitting forms.
This cookie is required for security reasons, to ensure that your browser is not being hijacked by third parties.</p>\n

IDE 的信息说请求需要 CSRF cookie。但到目前为止,在论坛、Doc 上阅读的所有内容都与价值本身更相关。这意味着您所需要的只是将 Header 中的 csrf 值发送为 X-Csrftoken 左右,View 就会发挥作用。


问题

因此我的问题是:

我是否必须在我的 ApolloClient 中同时设置 X-CsrftokenCookie:..csrftoken 才能在我的 django GraphQLView 上发出请求?

或者是否也可以只发送 X-Csrftoken 而没有 csrf-cookie,反之亦然?

经过长时间的停顿来关注这个问题,我再次尝试并找到了解决方案。

设置

  • django 3.1
  • vue 2.6
  • vue-apollo 3.0.4(支持新的Apollo-Client 3)
  • @apollo/client3.1.3

推测

  • 我将 Vue 用作多应用程序而不是单个应用程序。
  • 当我在 Django STATICFILES_DIRS 中写入我的 *vue.js 文件时,Webpack DevServer 将 Hot-Reload。 Django 将从那里获取文件。工作正常

问题回顾

重新审视我的问题后,我发现我有 2 个问题。一个是浏览器因为 CORS 而拒绝了 graphQL 请求。第二个是 CSRF 令牌。


解决方案

为了修复 CORS 问题,我注意到我的 uri Apollo Client 与我的 Django Dev Server 不同。它被设置为 http://localhost:7000/graphql 而不是 http://127.0.0.1:7000/graphql。我还设置了 credentials(参见 vue-apollo.js)

为了修复 CSRF,我做了 3 件事

  • 确保发送带有 HTML 的 {% csrf_token %},您的 Vue/GraphQL 客户端应用程序已挂钩。以便我们稍后取回。
  • 安装 js-cookie 以获取 Cookie
  • 在 Apollo Client Constructor 中设置 header,在 vue-apollo.js
  • 中设置 X-CSRFToken

vue-apollo.js


import Vue from 'vue'
// import path for the new Apollo Client 3 and Vue-Apollo
import { ApolloClient, InMemoryCache } from '@apollo/client/core';
import VueApollo from 'vue-apollo'
import Cookies from 'js-cookie'

  
// Create the apollo client
const apolloClient = new ApolloClient({
  // -------------------
  // # Required Fields #
  // -------------------
  // URI - GraphQL Endpoint
  uri: 'http://127.0.0.1:7000/graphql',
  // Cache
  cache: new InMemoryCache(),

  // -------------------
  // # Optional Fields #
  // -------------------
  // DevBrowserConsole
  connectToDevTools: true,
  // Else
  credentials: 'same-origin',
  headers: {
    'X-CSRFToken': Cookies.get('csrftoken')
  }
});
  
// create Vue-Apollo Instance
const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
})
  
// Install the vue plugin
Vue.use(VueApollo)
  
export default apolloProvider

Vue.config.js


const BundleTracker = require("webpack-bundle-tracker");

// hook your apps
const pages = {
    'page_1': {
        entry: './src/page_1.js',
        chunks: ['chunk-vendors']
    },
    'page_2': {
        entry: './src/page_2.js',
        chunks: ['chunk-vendors']
    },
}

module.exports = {
    pages: pages,
    filenameHashing: false,
    productionSourceMap: false,

    // puplicPath: 
    // Tells Django where do find the bundle.
    publicPath: '/static/',

    // outputDir:
    // The directory where the production build files will be generated - STATICFILES_DIRS
    outputDir: '../dev_static/vue_bundle',
 
    
    chainWebpack: config => {

        config.optimization
        .splitChunks({
            cacheGroups: {
                vendor: {
                    test: /[\/]node_modules[\/]/,
                    name: "chunk-vendors",
                    chunks: "all",
                    priority: 1
                },
            },
        });


        // Don´t create Templates because we using Django Templates
        Object.keys(pages).forEach(page => {
            config.plugins.delete(`html-${page}`);
            config.plugins.delete(`preload-${page}`);
            config.plugins.delete(`prefetch-${page}`);
        })

        // create webpack-stats.json. 
        // This file will describe the bundles produced by this build process.
        // used eventually by django-webpack-loader
        config
            .plugin('BundleTracker')
            .use(BundleTracker, [{filename: '/webpack-stats.json'}]);


        // added to use ApolloQuery Tag (Apollo Components) see vue-apollo documentation
        config.module
        .rule('vue')
        .use('vue-loader')
            .loader('vue-loader')
            .tap(options => {
            options.transpileOptions = {
                transforms: {
                dangerousTaggedTemplateString: true,
                },
            }
            return options
            })
        
        // This will allows us to reference paths to static 
        // files within our Vue component as <img src="~__STATIC__/logo.png">
        config.resolve.alias
            .set('__STATIC__', 'static')

        // configure a development server for use in non-production modes,
        config.devServer
            .public('http://localhost:8080')
            .host('localhost')
            .port(8080)
            .hotOnly(true)
            .watchOptions({poll: 1000})
            .https(false)
            .headers({"Access-Control-Allow-Origin": ["*"]})
        
        // DO have Webpack hash chunk filename
        config.output
            .chunkFilename("[id].js")
            },

    devServer: {
        writeToDisk: true
      }
};