Getting Django, VUE, CORS and CSRF working with a real world example

自作多情 提交于 2019-12-31 01:48:06

问题


I'm really stuck. Here's what I'm trying to do.

  1. KEEP CSRF On. - please don't tell me to turn it off.
  2. I have an API app run by Django and Django Rest Framework
  3. I have a frontend app run by Vue
  4. I have installed django-cors-headers to manage CORS

Everything works great localy. As soon as I move it to production, I start getting CSRF errors. Here's how everything works.

I've seen answers all over that have said everything from turning off CSRF to allowing all for all the things. I want to do this right and not just shut things off and open everything up and end up with a security hole.

So, here's what I have.

Installed: django-cors-headers django-rest-framework drf-nested-routers ... and others

I have the api running at api.websitename.com and the Vue.js app is running at websitename.com.

GET requests work great. OPTION requests seem to work.

Any risky request does not work.

For my CORS I have 'corsheaders.middleware.CorsMiddleware', installed before my other MIDDLEWARE.

Then my CORS settings are:

CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = (
    '*.websitename.com',
)

And my CSRF settings are:

CSRF_TRUSTED_ORIGINS = [
    "api.websitename.com",
]

No matter how I play with these, I end up with a CSRF token error.

I've tried the approach of doing something like this in my Vue App.vue file:

mounted () {
  this.getCSRFToken()
},
methods: {
  getCSRFToken () {
    return axios.get('token/').then(response => {
      axios.defaults.headers.common['x-csrftoken'] = Cookies.get('csrftoken')
    }).catch(error => {
      return Promise.reject(error.response.data)
    })
  }
}

The idea being that I get a CSRF token as soon as the APP loads in the browser. But even with that, I'm getting failed CSRF token errors when the app tries to do anything except a GET or OPTION.

Here's the view that returns the token incase youre curios:

class CSRFTokenView(APIView):
    permission_classes = (permissions.AllowAny,)

    @method_decorator(ensure_csrf_cookie)
    def get(self, request):
        return HttpResponse()

I realize I might be mixing problems here, but any suggestions that could help me trouble shoot this are welcome.


回答1:


First of all you want to use SessionAuthentication:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
    )
}

This will enforce CSRF, except for anonymous users (more on this in a bit). For browser frontends the easiest solution is to have both the (browser) frontend and backend under the same domain - this lets you avoid CORS - as suggested by comments above. If you have other clients then just go with tokens (DRF tokens or JWT) - but these are not safe for browser usage due to the danger of XSS attacks (storing tokens in localStorage is inherently insecure).

As you are using axios, CSRF setup is dead easy:

import axios from 'axios'

axios.defaults.xsrfHeaderName = 'X-CSRFToken'
axios.defaults.xsrfCookieName = 'csrftoken'

So you should have safe sessions with CSRF enforced. Almost. To quote the linked page above:

Warning: Always use Django's standard login view when creating login pages. This will ensure your login views are properly protected.

CSRF validation in REST framework works slightly differently to standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied.

This is icky - you either have to just use Django server-side views which makes your SPA design somewhat more complicated or recreate login and other auth views in DRF, with the caveat of using the @csrf_protect method decorator to enforce CSRF on these "anonymous" views. Obviously such views will break for token-using clients so you probably want to use different endpoints for these (maybe re-using the same base classes). So your browser login uses /auth/browser/login/ and your mobile login /auth/mobile/login/, the former wrapped using @csrf_protect.

Recreating login and other auth views from scratch should be done carefully after studying the contrib auth source code; for vanilla requirements I would recommend pre-existing solutions like django-rest-auth and django-all-auth. The django-rest-auth package however is not well designed for browser frontends and forces the usage of token generation, plus you would need to wrap the views as described above. On the other hand, all-auth provides AJAX responses for JS clients and might be a better bet.




回答2:


By far the easiest way to resolve this is to serve everything from the same domain. You can have your CDN or proxy direct /api calls to one server and the rest to the frontend server. This way there is no need to worry about CORS at all.

To get this working, I think you're just missing withCredentials = true in AXIOS configuration. Django requires the CSRF cookie to be sent and cookies are not sent over cross origin requests when withCredentials is not set.

axios.interceptors.request.use(function (config) {
  config.withCredentials = true
  return config
})

Another setting that might be missing is Djano's SESSION_COOKIE_DOMAIN. You should set it like this:

SESSION_COOKIE_DOMAIN=".mywebsite.com"

That first dot is important because it tells Django and then the web browser to use the cookie for *.mywebsite.com including api.mywebsite.com.

If it all still fails, I suggest setting a breakpoint on Django's CSRF middleware to see what's missing to make it work.



来源:https://stackoverflow.com/questions/54836387/getting-django-vue-cors-and-csrf-working-with-a-real-world-example

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!