《Tango with Django》-第10章 cookie 和会话

10.1 cookie 无处不在

Web 服务器收到请求后会返回所请求页面的内容。除了内容之外,还会返回一些 cookie。cookie可以理解为服务器发给客户端的少量信息。准备发送请求时,客户端检查有没有与服务器地址匹配的 cookie,如果有,随请求一起发送。服务器收到请求后把 cookie 放在请求的上下文中解释,然后生成合适的响应。
以使用用户名和密码登录网站为例。通过身份验证后,服务器可能会把一个包含用户名的 cookie发给浏览器,指明那个用户已经登录网站。随后的请求会连同这个信息一起发给服务器,从而渲染针对已登录用户的页面,例如在页面的某个位置显示用户名。然而,会话不是永久的,因为cookie 不会一直存在,而会在某一时刻过期。涉及敏感信息的 Web 应用可能会在几分钟之后就让cookie 过期。要求没这么严格的 Web 应用可能会在半小时之后,甚至几周之后让 cookie 过期。
以 cookie 的形式传递信息可能会导致 Web 应用存在安全漏洞,因此 Web 应用的开发者在使用cookie 时一定要谨慎。

10.2 会话和无状态协议

Web 浏览器(客户端)与服务器之间的通信都借由 HTTP 协议。前面说过,HTTP 是无状态的协议。因此,Web 浏览器所在的客户端电脑每次请求服务器中的资源(HTTP GET)或者把资源发给服务器(HTTP POST)都要建立新的网络连接(TCP 连接)。
由于客户端和服务器之间无法建立持久连接,所以两端的软件都不能只依靠连接维持会话状态。例如,客户端发送的每个请求都要指明哪个用户在当前电脑上已登录 Web 应用。这是客户端与服务器之间的一种对话,是半永久性信息交互机制(即会话)的基础。鉴于 HTTP 是无状态的协议,想维持会话状态还是有一定难度的,不过我们无需担心,现在有多种技术能解决这个问题。
维持状态最常用的一种方式是在客户端电脑的 cookie 中存储会话 ID。你可以把会话 ID 理解为一种令牌(一串字符,或一个字符串),我们通过它唯一标识 Web 应用中的会话。这种方式无需在客户端的 cookie 中存储大量信息(例如用户名、姓名或密码),存储的只是会话 ID。通过这个ID 可以从 Web 服务器中获取包含所需信息的数据结构。通过这种方式存储用户的信息更安全,不会由于客户端的漏洞或者连接被监听而导致信息泄露。
现代的浏览器,只要不特意关闭,都支持 cookie。你访问的几乎每个网站都会为你创建一个会话。不信的话现在你就可以确认,如图 10-2 所示。以 Google Chrome 为例,通过开发者工具便可以查看当前访问网站的 cookie。依次点击菜单“Chrome 设置 > 更多工具 > 开发者工具”,在打开的开发者工具面板中点击“Application”标签页,在左侧的“Storage”菜单中找到“Cookies”。如果你打开的是 Rango 应用的某个页面,应该会看到一个名为 sessionid 的 cookie。这个 cookie 的值是一系列字母和数字,Django 就是通过这个值唯一标识你电脑中的这个会话的。通过会话 ID 可以获取所有会话信息,只不过这些信息存储在服务器端。
在这里插入图片描述

10.3 在 Django 中设置会话

虽然你需要的功能可能已经设置好,拿来就能用,但最好知道 Django 的某个模块提供的是什么功能。本章所讲的会话,在 Django 中通过中间件实现。
为了确保万无一失,请打开 Django 项目的 settings.py 文件,找到 MIDDLEWARE 列表。这个列表中应该有个元素是 django.contrib.sessions.middleware.SessionMiddleware。如果没有,请自己动手加上。sessionid cookie 就是由 SessionMiddleware 中间件创建的。
SessionMiddleware 中间件支持多种存储会话信息的方式,可以存在文件中、数据库中,甚至是内存中。最简单的方法是使用 django.contrib.sessions 应用,把会话信息存储在模型/数据库中(模型为 django.contrib.sessions.models.Session)。若想使用这种方法,Django 项目的INSTALLED_APPS 设置中还要列出 django.contrib.sessions。别忘了,添加这个应用后还要使用迁移命令更新数据库。

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

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rango',
]

10.4 测试是否支持 cookie

虽然所有现代的 Web 浏览器都支持 cookie,但是在某些安全级别下可能会被禁用。因此,在使用cookie 之前要测试一下。不过,这一步基本上是多余的。
测试是否支持 cookie 时,可以使用 Django 的 request 对象提供的三个便利方法:
set_test_cookie()、test_cookie_worked() 和 delete_test_cookie()。我们要在一个视图中设定测试 cookie,然后在另一个视图中检查那个 cookie 是否存在。注意,必须使用两个不同的视图,因为我们要等到下一次请求才能确认客户端是否接受服务器发送的 cookie。
这里,我们将使用两个现有的视图做个简单的测试:index() 和 about()。但是我们不在页面中显示什么内容,而是通过 Django 开发服务器在终端里的输出判断是否支持 cookie。
打开 Rango 应用的 views.py 文件,找到 index() 视图。在视图中添加下述代码。为了确保这一行一定会被执行,请将其放在视图的第一行。

  • 打开 Rango 应用的 views.py 文件
def index(request):
    request.session.set_test_cookie()
    category_list = Category.objects.order_by('-likes')[:5]
    page_list = Page.objects.order_by('-views')[:5]
    context_dict = {
    
    'categories': category_list, 'pages': page_list}
    return render(request,'rango/index.html',context = context_dict)

def about(request):
    if request.session.test_cookie_worked():
        print("TEST COOKIE WORKED")
        request.session.delete_test_cookie()
    print(request.method)
    print(request.user)
    return render(request, 'rango/about.html', {
    
    })

最后,启动 Django 开发服务器,在浏览器中访问 Rango 应用的首页,接着再访问关于页面。在Django 开发服务器的控制台中应该会看到“TEST COOKIE WORKED!”,如图。
在这里插入图片描述

10.5 客户端 cookie:访问次数统计示例

知道 cookie 的工作原理之后,下面来实现一个非常简单的网站访问次数统计功能。为此,我们要创建两个 cookie:一个记录用户访问 Rango 应用的次数,另一个记录用户最后一次访问网站的时间。之所以记录最后访问时间,是因为我们只想一天增加一次访问次数,以防有人恶意刷次数。
假设有用户访问 Rango 应用的合理位置是首页。我们要定义一个函数,传入 request 和 response对象,让它处理 cookie。然后在 Rango 应用的 index() 视图中调用。打开 Rango 应用的 views.py文件,添加下述函数。注意,严格来说这不是视图函数,而是个辅助函数,因为它不返回response 对象。

  • Rango/ views.py
from datetime import datetime

def visitor_cookie_handler(request,response):
    # 获取网站的访问次数
    # 使用 COOKIES.get() 函数读取“visits”cookie
    # 如果目标 cookie 存在,把值转换为整数
    # 如果目标 cookie 不存在,返回默认值 1
    visits = int(request.COOKIES.get('visit','1'))

    last_visit_cookie = request.COOKIES.get('last_visit',str(datetime.now()))
    last_visit_time = datetime.strptime(last_visit_cookie[:-7],'%Y-%m-%d %H:%M:%S')

    # 如果距上次访问已超过一天……
    if (datetime.now() - last_visit_time).days > 0:
        visits = visits + 1
        # 增加访问次数后更新“last_visit”cookie
        response.set_cookie('last_visit',str(datetime.now()))
    else:
        # 设定“last_visit”cookie
        response.set_cookie('last_visit',last_visit_cookie)
    
    # 更新或设定“visits”cookie
    response.set_cookie('visits',visits)

这个辅助函数的参数有两个,分别为 request 和 response 对象,因为我们既需要从入站请求中读取 cookie,也需要把 cookie 添加到响应中。在这个函数中,我们调用了 request.COOKIES.get()函数,这也是一个辅助函数,由 Django 提供。如果指定的 cookie 存在,COOKIES.get() 函数返回cookie 的值;如果不存在,我们可以提供一个默认值。
得到两个 cookie 的值之后,计算两次访问的间隔有没有超过一天。如果无需相隔一天,可以把.days 改成 .seconds,这样只要相差一秒就能更新访问次数。
注意,所有 cookie 的值都是字符串。不要以为存储整数的 cookie 会返回整数类型的值。你要自行转换为正确的类型,因为 cookie 并不知道它存储的值是什么类型。
如果 cookie 不存在,可以在 response 对象上调用 set_cookie() 方法,创建一个 cookie。这个方法接受两个参数,一个是想创建的 cookie 名称(字符串形式),另一个是 cookie 的值。传入的cookie 值不限类型,set_cookie() 方法会自动将其转换为字符串。
这个函数用到了 datetime 模块,因此要在 views.py 文件的顶部将其导入。
然后更新 index() 视图,调用 visitor_cookie_handler() 辅助函数。为此,要先提取得到response 对象。

def index(request):
	request.session.set_test_cookie()
    category_list = Category.objects.order_by('-likes')[:5]
    page_list = Page.objects.order_by('-views')[:5]
    context_dict = {
    
    'categories': category_list, 'pages': page_list}
    
    # 提前获取 response 对象,以便添加 cookie
    response = render(request,'rango/index.html',context = context_dict)

    # 调用处理 cookie 的辅助函数
    visitor_cookie_handler(request,response)

    # 返回 response 对象,更新目标 cookie
    return response

现在,访问 Rango 应用的首页,然后打开浏览器中的 cookie 查看工具(例如 Google Chrome 的开发者工具),你应该能看到 visits 和 last_visit 两个 cookie,如图 10-4 所示。此外,还可以更新 index.html 模板,添加 < p>visits: { { visits }}

,显示访问次数。
  • index.html
<p>visits: {
    
    {
    
     visits }}</p>

在这里插入图片描述

在这里插入图片描述

10.6 会话数据

前一节的示例展示了如何存储和处理客户端 cookie,此时数据存储在客户端。然而,为了会话信息的安全,最好将其存储在服务器端,然后通过存储在客户端的会话 ID 访问会话数据。
使用基于会话的 cookie 的基本步骤如下:
➊ 确保 settings.py 模块中的 MIDDLEWARE_CLASSES 列表里有
django.contrib.sessions.middleware.SessionMiddleware。
➋ 配置会话后端。确保 settings.py 模块中的 INSTALLED_APPS 列表里有
django.contrib.sessions。如果没有,加上,然后运行数据库迁移命令 python manage.py migrate。
➌ 默认使用的是数据库后端,不过也可以设置为其他后端(例如缓存)。详情参见 Django 文 档。

基于会话的 cookie 不直接存储在请求中(因此也不保存在客户端设备中),读取使用request.session.get() 方法,存储新值使用 request.session[]。注意,为了记住客户端设备,依然要在客户端 cookie 中存储会话 ID。然而,用户/会话数据全部保存在服务器端。Django 提供的会话中间件能处理这两端的操作。
为了使用服务器端存储的数据,我们要重构前一节编写的代码。首先,更新visitor_cookie_handler() 函数,改为从服务器端存取 cookie:调用 request.session.get() 方法读取 cookie,通过 request.session[] 更新 cookie。简便起见,我们定义一个辅助函数,名为get_server_side_cookie(),让它从请求中读取 cookie,如果会话数据中有指定的 cookie,返回其值,否则返回默认值。
既然现在所有 cookie 都存储在服务器端,我们就不用直接修改响应了。鉴于此,
visitor_cookie_handler() 函数中的 response 可以删掉了。

def get_server_side_cookie(request, cookie, default_val=None):
    val = request.session.get(cookie)
    if not val:
        val = default_val
    return val


def visitor_cookie_handler(request,response):
    # 获取网站的访问次数
    # 使用 COOKIES.get() 函数读取“visits”cookie
    # 如果目标 cookie 存在,把值转换为整数
    # 如果目标 cookie 不存在,返回默认值 1
    visits = int(get_server_side_cookie(request,'visit','1'))

    last_visit_cookie = get_server_side_cookie(request,'last_visit',str(datetime.now()))
    last_visit_time = datetime.strptime(last_visit_cookie[:-7],'%Y-%m-%d %H:%M:%S')

    # 如果距上次访问已超过一天……
    if (datetime.now() - last_visit_time).days > 0:
        visits = visits + 1
        # 增加访问次数后更新“last_visit”cookie
        request.session['last_visit'] = str(datetime.now())
    else:
        # 设定“last_visit”cookie
        request.session['last_visit'] = last_visit_cookie
    
    # 更新或设定“visits”cookie
    request.session['visits'] = visits

更新完处理 cookie 的辅助函数之后,接下来要更新 index() 视图。首先,把
visitor_cookie_handler(request, response) 改成 visitor_cookie_handler(request)。然后,添加下面这行,把访问次数传入上下文字典。

这一行要在调用 render() 函数之前执行,否则无效。修改后的 index() 视图如下所示。

def index(request):
    request.session.set_test_cookie()
    category_list = Category.objects.order_by('-likes')[:5]
    page_list = Page.objects.order_by('-views')[:5]
    context_dict = {
    
    'categories': category_list, 'pages': page_list}

    visitor_cookie_handler(request)
    context_dict['visits'] = request.session['visits']
    
    response = render(request,'rango/index.html',context = context_dict)
    
    return response

在重启 Django 开发服务器之前先把客户端现有的 cookie 删除。

10.7 浏览器存续期会话和持久会话

Django 的会话框架设定的会话分为浏览器存续期会话和持久会话两种。
❏ 浏览器存续期会话在用户关闭浏览器后过期
❏ 持久会话不在浏览器关闭后过期,而是由你自己指定过期时间,可以是半个小时,甚至几个月
默认情况下,浏览器存续期会话是禁用的。若想启用,打开 Django 项目的 settings.py 模块,添加SESSION_EXPIRE_AT_BROWSER_CLOSE 变量,把值设为 True。
默认启用的是持久会话,此时 SESSION_EXPIRE_AT_BROWSER_CLOSE 的值为 False,或者不存在。持久会话还有个设置,SESSION_COOKIE_AGE,用于设定 cookie 的存活期。这个设置的值是一个整数,表示 cookie 存活的秒数。例如,如果把值设为 1209600,网站的 cookie 将在两周(14 天)后过期。可用选项的详细说明参阅 Django 文档。

10.8 清理会话数据库

会话不断增多,存储会话信息的数据存储器随之不断增大。如果使用数据库存储 Django 会话,要定期清理数据库。使用的命令是 python manage.py clearsessions。Django 文档建议通过 cron 作业每天运行一次这个命令。如若不然,随着用户数量的增多,你会发现应用的性能每况愈下。

10.9 注意事项和基本流程

  • 在 Django 应用中使用 cookie 时要注意以下几点:
    ❏ 首先,确定你的 Web 应用需要哪种 cookie。你想存储的信息需要在浏览器关闭后留存吗,能在会话结束后弃之不用吗?
    ❏ 审慎决定要在 cookie 中存储哪些信息。记住,cookie 中的信息保存在客户端电脑中,这隐藏着巨大的安全隐患,毕竟你对用户电脑的安全保护措施一无所知。涉及敏感信息时,应该考虑使用服务器端会话。
    ❏ 用户可能会把浏览器的安全设置设为较高的级别,禁止使用 cookie,从而导致网站的功能失效。一定要考虑这种情况,因为你对用户的浏览器设置没有控制权。

  • 如果决定使用客户端 cookie,遵照下述步骤操作:
    ➊ 必须先检查想使用的 cookie 是否存在。request.COOKIES.has_key(’<cookie_name>’) 函数返回一个布尔值,指明名为 <cookie_name> 的 cookie 是否存在于客户端电脑中。
    ➋ 如果目标 cookie 存在,便可以读取其值:request.COOKIES[]。COOKIES 属性的值是个字典,因此想读取 cookie 时,把 cookie 的名称(字符串形式)传入方括号中。记住,cookie的值始终为字符串,不管存储的值是什么语义。因此,要做好转换类型的准备(例如使用int() 或 float())。
    ➌ 如果目标 cookie 不存在,或者想更新 cookie,把想保存的值传给生成的响应。要调用的函数是 response.set_cookie(’<cookie_name>’, value),第一个参数是 cookie 的名称,第二个参数是要设定的值。

  • 如果对安全要求更高,应该使用基于会话的 cookie。
    ➊ 首先,确保 Django 项目的 settings.py 模块中的 MIDDLEWARE_CLASSES 列表里有
    django.contrib.sessions.middleware.SessionMiddleware。如果没有,自行加上。
    ➋ 使用 SESSION_ENGINE 配置会话后端。不同的后端配置参见 Django 文档。
    ➌ 通过 requests.sessions.get() 检查 cookie 是否存在。
    ➍ 通过会话字典更新或设定 cookie:requests.session[’<cookie_name>’]。

练习

在这里插入图片描述

  • 确认你的cookie存储在服务器端。清空浏览器的缓存和cookie,然后确认浏览器中没有last_visit和visits两个cookie。注意,sessionid cookie应该还有。Django通过这个cookie从数据库中检索存储在服务器端的会话数据。
    -更新about页面的视图和模板,告诉访客他们访问了多少次网站。记得先调用visitor_cookie_handler()函数,再从request.session字典中读取visits cookie。如若不然, visits cookie不存在的话,程序会抛出错误。
    rango/views.py
def about(request):
    if request.session.test_cookie_worked():
        print("TEST COOKIE WORKED")
        request.session.delete_test_cookie()
    print(request.method)
    print(request.user)

    category_list = Category.objects.order_by('-likes')[:5]
    page_list = Page.objects.order_by('-views')[:5]
    context_dict = {
    
    'categories': category_list, 'pages': page_list}

    # 调用处理 cookie 的辅助函数
    visitor_cookie_handler(request)
    context_dict['visits'] = request.session['visits']

    # 提前获取 response 对象,以便添加 cookie
    response = render(request,'rango/about.html',context = context_dict)

    # 返回 response 对象,更新目标 cookie
    return response

template/about.html

<p>visits: {
    
    {
    
     visits }}</p>

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_46629123/article/details/113139806