django session_key 每次 ajax 调用都不同

django session_key different on each ajax call

我有一个只能通过 AJAX 访问的 Django 应用程序。我的主要问题是我想获得一个唯一 ID,该 ID 与发出请求的特定浏览器实例配对。

为了尝试做到这一点,我试图访问 django 创建的 session_key,但它有时会以 None.

的形式返回

以下是我在 Django 中创建 JSON 响应的方式:

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)

当我使用 Postman 发出请求时,session_key 出现在 JSON 正文和 cookie 中,但是当我通过 jquery 在浏览器中发出请求时,request.session.session_key 是 None,这就是我添加这些行的原因:

if not request.session.exists(request.session.session_key):
    request.session.create()

但是当我这样做时,session_key 每次都不一样。

我是这样调用 AJAX 的:

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

  })
}

Django 文档说会话并不总是被创建:

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

有没有一种方法可以强制创建 django session_key,即使会话未被修改,但在不应该更改时却不更改?或者有没有办法 "modify the session" 像 Postman 那样正确地创建它?

您的问题基本上来自您请求的 cross-domain 部分。我复制了你的示例,并尝试通过 localhost 访问主页,然后在 localhost 上发送 Ajax 请求,并且 session 密钥被保存。

但是,当我将 Ajax 请求更改为 127.0.0.1(并正确配置 django-cors-headers)时,我会遇到与您描述的完全相同的问题. session 键随每个请求而改变。 (CSRF 令牌也是如此,但这是故意的,应该保持这种方式。)

在这里,您混合了 CORS, third-party cookiesXMLHttpRequest 中的凭据,这让整个事情都崩溃了。

到底发生了什么?

上下文

首先,让我们就您的文件内容达成一致。

# 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)

更大的 HTML 页面中包含的 Javascript 代码:

# 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);
    })
}

请注意,Ajax 请求是在 127.0.0.1 上发出的,而不是在 localhost 上发出的,就像您的情况一样。 (或者,您可以在 127.0.0.1 上访问主 HTML 页面并在 Ajax 调用中使用 localhost,但想法是一样的。)

最后,通过在 INSTALLED_APPSMIDDLEWARE 中添加足够的行来在 settings.py 中允许 CORS(有关安装的更多信息,请参阅 django-cors-headers ).为方便起见,您允许所有 URL 和所有 ORIGIN,方法是添加以下行:

# 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.

机制

  1. 您的浏览器向主页发送一个GET请求(假设它是网站的根,请求是GET http://localhost

在这里,如果 session 在您的浏览器和 localhost 之间打开并不重要,因为状态可能不会改变。

服务器发回包含 Javascript 代码的 HTML 页面。

  1. 您的浏览器解释 Javascript 代码。它创建一个 XMLHttpRequest,发送到地址 http://127.0.0.1/ajax_call 的服务器。如果一个session之前是用localhost打开的,这里就不能用了,因为域名不一样。无论如何,没关系,因为在回答这个请求时可以创建session。

  2. 服务器收到 127.0.0.1 的请求。如果您没有将此主机添加为 settings.py 中的 allowed-host 并且您 运行 您的服务器处于生产模式,则会引发异常。故事的结局。如果您在 DEBUG 模式下 运行,服务器会创建一个新的 session,因为 none 是由客户端发送的。一切如期进行,一个新的 session_key 和一个新的 CSRF token 被创建,并发送到 header 中的客户端。让我们更准确地看一下。

浏览器发送的header示例如下:

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

仔细查看 Cookie 行。没有发送 sessionid,因为没有 session 是打开的。

如果一切顺利,服务器发回的 header 看起来就是这样。

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

服务器创建了一个session,并将所有需要的信息发送给客户端,作为Set-Cookieheader。看起来不错!

  1. 我们期望浏览器的行为是为域 127.0.0.1 设置 cookie,并将其用于同一域上的下一个请求。但是,查看一个新的请求header显示它没有发送header中的sessionid,使得服务器创建一个新的session,发送新的信息作为Set-Cookie header,重新开始这个过程。如果您查看计算机上存储的 cookie(使用 Firefox,右键单击页面 > View Page Info,然后选择 Security 选项卡,单击 View Cookies),您可以看到一些 cookie localhost,但 127.0.0.1 没有。浏览器不会设置从 Ajax 调用中获取的 cookie。

为什么?

原因太多了! 我们将在解决方案部分列出它们。因为解决这个问题最简单最好的方法不是解锁它,而是正确使用它。

如何解决?

  1. 这是第一个、最好、最简单的推荐解决方案:使您的主域和 Ajax 调用域匹配。您将避免 CORS 头痛,避免打开潜在的安全漏洞,不处理 third-party cookie,并使您的开发环境与您的生产环境保持一致。

  2. 如果你真的想处理两个不同的域,问问自己是否需要。如果没有,回到1。如果你真的需要,我们来看看解决方案。

    • 首先,请确保您的浏览器接受 third-party cookie。对于 Firefox,在 Preferences 中,转到 Privacy 选项卡。在 History 部分,select Firefox will: Use custom settings for history,并检查 Accept third-party cookies 是否设置为 Always。如果访问者禁用 third-party cookie,您的网站将因他而损坏。 (转到解决方案 1 的第一个充分理由。)
    • 其次,当收到来自 Ajax 调用的回复时,您必须告诉您的浏览器不要忽略 Cookie 设置。将 withCredentials 设置添加到 xhr 是解决方案。这是新的 Javascript 代码:

      function call () {
          $.ajax({
              type: 'GET',
              xhrFields: {
                  withCredentials: true
              },
              url: 'http://127.0.0.1/ajax_call/'
          }). [...]
      
    • 如果您现在尝试,您会发现浏览器仍然不满意。原因是

      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 ‘*’).

      这是一个安全功能。使用此配置,您可以再次打开已被 CORS 保护阻止的安全后膛。您允许任何网站发送经过身份验证的 CORS 请求。 永远不要那样做。 使用 CORS_ORIGIN_WHITELIST 而不是 CORS_ORIGIN_ALLOW_ALL 可以解决此问题。

    • 还没有完成。您的浏览器现在抱怨其他事情:

      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’).

      同样,这是一项安全功能。通过 CORS 请求发送凭据在大多数情况下 非常糟糕 。您必须在资源端手动允许它。您可以通过激活 CORS_ALLOW_CREDENTIALS 设置来实现。您的 settings.py 文件现在看起来像这样:

      CORS_URLS_REGEX = r'.*'
      CORS_ORIGIN_WHITELIST = (
          'localhost',
          '127.0.0.1',
      )
      CORS_ALLOW_CREDENTIALS = True
      
    • 现在,您可以再试一次,看看它是否有效,然后再问问自己是否真的需要在 Ajax 调用中使用不同的域。