问题
I've got a django application that I am accessing only over AJAX. My main problem is I want to get a unique ID which pairs to a particular browser instance making the request.
To try to do this, I'm trying to access the session_key
that django creates, but it's sometimes coming back as None
.
Here's how I'm creating the JSON response in django:
def get(self, request, pk, format=None):
resp_obj = {}
...
resp_obj['csrftoken'] = csrftoken
# shouldn't need the next two lines, but request.session.session_key is None sometimes
if not request.session.exists(request.session.session_key):
request.session.create()
resp_obj['sessionid'] = request.session.session_key
return JSONResponse(resp_obj)
When I make the request using Postman, the session_key
comes through in both the JSON body and in the cookie, but when I make the request through jquery in a browser, request.session.session_key
is None, which is why I added these lines:
if not request.session.exists(request.session.session_key):
request.session.create()
But when I do that, the session_key
is different each time.
Here's how I'm making the AJAX call:
for (var i = 0; i < this.survey_ids.length; i++) {
$.ajax({
url: this.SERVER_URL+ '/surveys/' + this.survey_ids[i] + '/?language=' + VTA.user_language,
headers: {
'Accept-Language': user_language
}
}).error(function (jqXHR, textStatus, errorThrown) {
// handle the error
}).done(function (response, textStatus, jqXHR) {
window.console.log(response.csrftoken) // different on each iteration
window.console.log(response.sessionid) // also different on each iteration
//handle response
})
}
The Django documentation says that sessions are not always created:
By default, Django only saves to the session database when the session has been modified – that is if any of its dictionary values have been assigned or deleted
https://docs.djangoproject.com/en/1.9/topics/http/sessions/#when-sessions-are-saved
Is there a way to force django session_key
creation even when is session not modified, but not have it change when it shouldn't? Or is there a way to "modify the session" such that it gets created properly like Postman is doing?
回答1:
Your problem basically comes from the cross-domain part of your request. I copied your example, and tried by accessing the main page with localhost
, then sending the Ajax
request on localhost
as well, and the session key is conserved.
However, when I change the Ajax
request to be on 127.0.0.1
(and with a proper configuration of django-cors-headers
), I can get the exact same issue as what you describe. The session key is changed for each request. (The CSRF token as well, but this is on purpose, and should be kept this way.)
Here, you have a mix of CORS, third-party cookies, and credentials in XMLHttpRequest
which makes the whole thing break.
What exactly is happening?
Context
First, let's agree on the content of your files.
# views.py
from django.http import JsonResponse
from django.middleware import csrf
def ajax_call(request):
if not request.session.exists(request.session.session_key):
request.session.create()
# To debug the server side
print request.session.session_key
print csrf.get_token(request)
# To debug the client side
resp_obj = {}
resp_obj['sessionid'] = request.session.session_key
resp_obj['csrf'] = csrf.get_token(request)
return JsonResponse(resp_obj)
The Javascript code included in a larger HTML page:
# Javascript, client side
function call () {
$.ajax({
type: 'GET',
xhrFields: {
withCredentials: true
},
url: 'http://127.0.0.1/ajax_call/'
}).fail(function () {
console.log("Error");
}).done(function (response, textStatus, jqXHR) {
console.log("Success");
console.log(response.sessionid);
console.log(response.csrf);
})
}
Note the Ajax request is made on 127.0.0.1
, and not on localhost
, as it is in your case. (Or maybe, you access the main HTML page on 127.0.0.1
and use localhost
in the Ajax call, but the idea is the same.)
Finally, you allow CORS
in settings.py
by adding the adequate lines in INSTALLED_APPS
and MIDDLEWARE
(see django-cors-headers for more info on the installation). To make it easy, you allow all URLs, and all ORIGIN
by adding these lines:
# settings.py
CORS_ORIGIN_ALLOW_ALL = True
CORS_URLS_REGEX = True
Do not copy these lines on a production server if you do not understand what it does! It can open a sever security breech.
Mechanisms
- Your browser sends a
GET
request to the main page (let's imagine it is the root of the website, and the request isGET http://localhost
)
Here, it does not really matter if a session is open between your browser and localhost
, as the state probably won't change.
The server sends back the HTML page with the Javascript code included.
Your browser interprets the Javascript code. It creates an
XMLHttpRequest
, which is sent to the server at the addresshttp://127.0.0.1/ajax_call
. If a session was previously open withlocalhost
, it cannot be used here, because the domain name is not the same. Anyway, it does not matter, as the session can be created when answering this request.The server receives a request for
127.0.0.1
. If you did not add this host as an allowed-host in yoursettings.py
and you run your server in production mode, an exception is raised. End of the story. If you run inDEBUG
mode, the server creates a new session, as none was sent by the client. Everything goes as expected, a newsession_key
and a newCSRF token
are created, and sent to the client in the header. Let's look at that more precisely.
Here is an example of the header sent by the browser:
Host: localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost/
X-Requested-With: XMLHttpRequest
Cookie: csrftoken=XCadvu4MJjDMzCkOTTC276oJs9P0j989CBw6rnidz7cS34PoOt1VftqWMqd8BHMX; django_language=en
DNT: 1
Connection: keep-alive
Look closely at the Cookie
line. No sessionid
is sent, because no session is open.
The header sent back by the server looks like that, if everything went well.
Content-Length: 125
Content-Type: application/json
Date: Sun, 17 Sep 2017 14:21:23 GMT
Server: WSGIServer/0.1 Python/2.7.14rc1
Set-Cookie: csrftoken=XCadvu4MJjDMzCkOTTC276oJs9P0j989CBw6rnidz7cS34PoOt1VftqWMqd8BHMX; expires=Sun, 16-Sep-2018 14:21:22 GMT; Max-Age=31449600; Path=/
sessionid=l505q4y8pywe9t3q76204662a1225scx; expires=Sun, 01-Oct-2017 14:21:22 GMT; httponly; Max-Age=1209600; Path=/
Vary: Cookie
x-frame-options: SAMEORIGIN
The server created a session, and sent all the needed information to the client, as a Set-Cookie
header. Looks good !
- The behavior we expect from the browser is that it sets the cookie for the domain 127.0.0.1, and uses it for the next request on the same domain. However, looking at a new request header shows that it does not sent the
sessionid
in the header, making the server create a new session, send the new information as aSet-Cookie
header, and start over the process. If you look at the stored cookies on your computer (with Firefox, right click on the page >View Page Info
, thenSecurity
tab, click onView Cookies
), you can see some cookies forlocalhost
, but nothing for127.0.0.1
. The browser does not set the cookie that it gets from the Ajax call.
Why?
For so many reason! We'll list them all on the Solution section. Because the easiest and best way to solve that is not to unlock it, but to use it correctly.
How to solve it?
Here is the first, best, easiest, recommended solution: make your main domain and Ajax call domain match. You will avoid the
CORS
headache, avoid opening a potential security breech, do not handle third-party cookies, and make your development environment consistent with your production.If you really want to handle two different domains, ask yourself if you need it. If you don't, go back to 1. If you really really need it, let's check the solution.
- First, be sure that your browser accepts third-party cookies. For Firefox, in
Preferences
, go toPrivacy
tab. In theHistory
section, selectFirefox will: Use custom settings for history
, and check ifAccept third-party cookies
is set asAlways
. If a visitor disables third-party cookies, your site will be broken for him. (First good reason to go to solution 1.) Second, you have to tell your browser not to ignore the Cookie setting when receiving the reply from the Ajax call. Adding withCredentials setting to your
xhr
is the solution. Here is the new Javascript code:function call () { $.ajax({ type: 'GET', xhrFields: { withCredentials: true }, url: 'http://127.0.0.1/ajax_call/' }). [...]
If you try right now, you'll see the browser is still not happy. The reason is
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘http://127.0.0.1/ajax_call/’. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’).
This is a security feature. With this configuration, you open again the security breech which has been blocked by the
CORS
protection. You allow any website to send an authenticatedCORS
request. Never do that. UsingCORS_ORIGIN_WHITELIST
instead ofCORS_ORIGIN_ALLOW_ALL
fixes this issue.Still not done. Your browser now complains for something else:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://127.0.0.1:8000/ajax_call/. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).
Again, this is a security feature. Sending credentials through a
CORS
request is most of the time really bad. You have to manually allow it on the resource side. You can do it by activating theCORS_ALLOW_CREDENTIALS
setting. Yoursettings.py
file now looks like this:CORS_URLS_REGEX = r'.*' CORS_ORIGIN_WHITELIST = ( 'localhost', '127.0.0.1', ) CORS_ALLOW_CREDENTIALS = True
Now, you can try again, see that it works, and ask yourself again if you really need to have a different domain in your Ajax call.
- First, be sure that your browser accepts third-party cookies. For Firefox, in
来源:https://stackoverflow.com/questions/37711565/django-session-key-different-on-each-ajax-call