赵走x博客
网站访问量:151521
首页
书籍
软件
工具
古诗词
搜索
登录
17、部署 Django 项目
16、自动化测试
14、jQuery 和 Django
13、Webhose 搜索
12、集成 Bootstrap
11、使用 Django-Registration-Redux
10、cookie 和会话
9、用户身份验证
8、模板进阶
7、表单
6、模型、模板和视图
5、模型与数据库
4、模板和媒体文件
3、Django 基础
2、前期准备工作
1、导言
10、cookie 和会话
资源编号:76316
Tango with Django
Python Web
热度:107
本章介绍会话处理和 cookie 存储的基础知识。会话和 cookie 是互不可分的两个概念,对如今的 Web 应用极为重要。前一章涉及的登录和退出功能就是基于会话和 cookie 实现的。然而,具体过 程被 Django 框架隐藏了。本章将探讨这背后到底发生了什么,以及 cookie 还能用来做什么。
本章介绍会话处理和 cookie 存储的基础知识。会话和 cookie 是互不可分的两个概念,对如今的 Web 应用极为重要。前一章涉及的登录和退出功能就是基于会话和 cookie 实现的。然而,具体过 程被 Django 框架隐藏了。本章将探讨这背后到底发生了什么,以及 cookie 还能用来做什么。 # 10.1 cookie 无处不在 Web 服务器收到请求后会返回所请求页面的内容。除了内容之外,还会返回一些 cookie。cookie 可以理解为服务器发给客户端的少量信息。准备发送请求时,客户端检查有没有与服务器地址匹 配的 cookie,如果有,随请求一起发送。服务器收到请求后把 cookie 放在请求的上下文中解释, 然后生成合适的响应。 以使用用户名和密码登录网站为例。通过身份验证后,服务器可能会把一个包含用户名的 cookie 发给浏览器,指明那个用户已经登录网站。随后的请求会连同这个信息一起发给服务器,从而渲 染针对已登录用户的页面,例如在页面的某个位置显示用户名。然而,会话不是永久的,因为 cookie 不会一直存在,而会在某一时刻过期。涉及敏感信息的 Web 应用可能会在几分钟之后就让 cookie 过期。要求没这么严格的 Web 应用可能会在半小时之后,甚至几周之后让 cookie 过期。 >★ cookie 缘起 ★ cookie 这个词不是取自食物, 而是来自“magic cookie”(收到后不经修改就发送的数据 包)。1994 年,MCI 向 Netscape Communications 提交了一个提案,描述了一种跨 HTTP 请 求的持久存储方式。这个提案源自他们开发的电子商务解决方案需要一种可靠的方式存储用 户购物篮中的内容。Netscape 的程序员 Lou Montulli 实现了这个提案,将其命名为 magic cookie。 关于 cookie 及其历史的更多信息,请阅读[维基百科](http://en.wikipedia.org/wiki/HTTP_cookie#History "维基百科")。当然,这个伟大提议是有软件专利的, 详情参见 Montulli 在美国申请的 [5774670 号专利](http://patft.uspto.gov/netacgi/nph-Parser?Sect1=PTO1&Sect2=HITOFF&d=PALL&p=1&u=/netahtml/PTO/srchnum.htm&r=1&f=G&l=50&s1=5774670.PN.&OS=PN/5774670&RS=PN/5774670 "5774670 号专利")。 以 cookie 的形式传递信息可能会导致 Web 应用存在安全漏洞,因此 Web 应用的开发者在使用 cookie 时一定要谨慎。想使用 cookie 时,一定要问自己:真的有必要把这个信息存储在客户端的 cookie 中吗?很多情况下,有更为安全的方案。比如说,电商网站通过 cookie 传送用户的信用卡 卡号就极不安全。试想,如果用户的电脑被攻击了呢?恶意程序可能会攫取 cookie,致使用户的 信用卡卡号泄露——这全是因为 Web 应用的设计有缺陷。本章将介绍客户端 cookie 和服务器端 会话存储的基础知识。 >◼ 欧盟对 cookie 的要求 ◼ 2011 年,欧盟发布了一部[关于 cookie 的法律](https://ico.org.uk/for-organisations/guide-to-pecr/cookies-and-similar-technologies/ "关于 cookie 的法律")。根据此部法律,存贮在欧盟境内的网站,在 用户首次访问时应该显示一个关于使用 cookie 的提醒消息。图 10-1 是 BBC 新闻网站显示的 提醒消息。 对开发网站的你来说,一定要知道这部法律,以及其他与可访问性有关的法律。  图 10-1:BBC 新闻网站(存贮在英国)顶部显示的 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 可以获 取所有会话信息,只不过这些信息存储在服务器端。 >★ 不使用 cookie 的方式 ★ 持久存储状态信息的另一种方式是在 URL 中编码会话 ID。例如,你可能见过这样的 PHP 网 页地址:http://www.site.com/index.php?sessid=someseeminglyrandomandlongstring1234 。这种 方式不在客户端设备中存储 cookie,但是 URL 不太好看。这与 Django 推崇的人类可读的简 洁 URL 相悖。  图 10-2:在 Google Chrome 的开发者工具中查看 sessionid cookie # 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。别忘了,添加这个应用后还要使用迁 移命令更新数据库。 # 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() 视图。在视图中添加下述代码。为了确保这一行 一定会被执行,请将其放在视图的第一行。 ``` request.session.set_test_cookie() ``` 然后找到 about() 视图,把下面三行添加到函数前部: ``` if request.session.test_cookie_worked(): print("TEST COOKIE WORKED") request.session.delete_test_cookie() ``` 最后,启动 Django 开发服务器,在浏览器中访问 Rango 应用的首页,接着再访问关于页面。在 Django 开发服务器的控制台中应该会看到“TEST COOKIE WORKED!”,如图 10-3 所示。  图 10-3:Django 开发服务器的控制台中显示有“TEST COOKIE WORKED!”消息 如果没显示上述消息,请检查浏览器的安全设置。某些设置可能会导致浏览器不接受 cookie。 # 10.5 客户端 cookie:访问次数统计示例 知道 cookie 的工作原理之后,下面来实现一个非常简单的网站访问次数统计功能。为此,我们要 创建两个 cookie:一个记录用户访问 Rango 应用的次数,另一个记录用户最后一次访问网站的时 间。之所以记录最后访问时间,是因为我们只想一天增加一次访问次数,以防有人恶意刷次数。 假设有用户访问 Rango 应用的合理位置是首页。我们要定义一个函数,传入 request 和 response 对象,让它处理 cookie。然后在 Rango 应用的 index() 视图中调用。打开 Rango 应用的 views.py 文件,添加下述函数。注意,严格来说这不是视图函数,而是个辅助函数,因为它不返回 response 对象。 ``` def visitor_cookie_handler(request,response): # 获取网站的访问次数 # 使用 COOKIES.get() 函数读取“visits”cookie # 如果目标 cookie 存在,把值转换为整数 # 如果目标 cookie 不存在,返回默认值 1 visit=int(request.COOKIES.get('visits',1)) last_visit_cookie=request.COOKIES.get('last_visit',str(datetime.datetime.now())) last_visit_time=datetime.datetime.strptime(last_visit_cookie[:-7],'%Y-%m-%d %H:%M:%S') # 如果距上次访问已超过一天…… if(datetime.datetime.now()-last_visit_time).days>0: visit=visit+1 # 增加访问次数后更新“last_visit”cookie response.set_cookie('last_visit',str(datetime.datetime.now())) else: # 设定“last_visit”cookie response.set_cookie('last_visit', str(datetime.datetime.now())) # 更新或设定“visits”cookie response.set_cookie('visits',visit) ``` 这个辅助函数的参数有两个,分别为 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 文件的顶部将其导入。 ``` import datetime ``` 然后更新 index() 视图,调用 visitor_cookie_handler() 辅助函数。为此,要先提取得到 response 对象。 ``` def index(request): request.session.set_test_cookie() # 查询数据库,获取目前存储的所有分类 # 按点赞次数倒序排列分类 # 获取前 5 个分类(如果分类数少于 5 个,那就获取全部) # 把分类列表放入 context_dict 字典 # 稍后传给模板引擎 category_list = Category.objects.order_by('-likes')[:5] page_list = Page.objects.order_by('-views')[:5] context_dict = { 'categories': category_list, 'page_list': page_list } # 提前获取 response 对象,以便添加 cookie response = render(request, 'rango/index.html', context_dict) # 调用处理 cookie 的辅助函数 visitor_cookie_handler(request, response) # 渲染响应,发给客户端 # 返回 response 对象,更新目标 cookie return response ``` 现在,访问 Rango 应用的首页,然后打开浏览器中的 cookie 查看工具(例如 Google Chrome 的开 发者工具),你应该能看到 visits 和 last_visit 两个 cookie,如图 10-4 所示。此外,还可以更 新 index.html 模板,添加 `
visits: {{ visits }}
`,显示访问次数。  图 10-4:在 Google Chrome 中打开开发者工具查看 Rango 应用的 cookie;留意 visits cookie,其值表明一共访问了三次,而且每次至少相隔一天 # 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): # 获取网站的访问次数 # 使用 COOKIES.get() 函数读取“visits”cookie # 如果目标 cookie 存在,把值转换为整数 # 如果目标 cookie 不存在,返回默认值 1 visit = int(request.COOKIES.get('visits', 1)) last_visit_cookie = request.COOKIES.get('last_visit', str(datetime.datetime.now())) last_visit_time = datetime.datetime.strptime(last_visit_cookie[:-7], '%Y-%m-%d %H:%M:%S') # 如果距上次访问已超过一天…… if (datetime.datetime.now() - last_visit_time).days > 0: visit = visit + 1 # 增加访问次数后更新“last_visit”cookie request.session['last_visit'] = str(datetime.datetime.now()) else: # 设定“last_visit”cookie request.session['last_visit'] = str(datetime.datetime.now()) # 更新或设定“visits”cookie request.session['visits'] = visit ``` 更新完处理 cookie 的辅助函数之后,接下来要更新 index() 视图。首先,把 ``` visitor_cookie_handler(request, response) ``` 改成 ``` visitor_cookie_handler(request) ``` 然后,添 加下面这行,把访问次数传入上下文字典。 ``` context_dict['visits'] = request.session['visits'] ``` 这一行要在调用 render() 函数之前执行,否则无效。修改后的 index() 视图如下所示。 ``` def index(request): request.session.set_test_cookie() # 查询数据库,获取目前存储的所有分类 # 按点赞次数倒序排列分类 # 获取前 5 个分类(如果分类数少于 5 个,那就获取全部) # 把分类列表放入 context_dict 字典 # 稍后传给模板引擎 category_list = Category.objects.order_by('-likes')[:5] page_list = Page.objects.order_by('-views')[:5] # 调用处理 cookie 的辅助函数 visitor_cookie_handler(request) context_dict = { 'categories': category_list, 'page_list': page_list, 'visits':request.session['visits'] } # 提前获取 response 对象,以便添加 cookie response = render(request, 'rango/index.html', context_dict) ``` 在重启 Django 开发服务器之前先把客户端现有的 cookie 删除。详情参见下面的提醒。 >★ cookie 的数据类型 ★ 把会话数据存储在服务器端的额外好处是,可以把字符串数据转换成所需的类型。不过这只 对内置类型有效,例如 int、float、long、complex 和 boolean。如果想存储字典或其他复杂 的类型,可以考虑[序列化对象](https://wiki.python.org/moin/UsingPickle "序列化对象")。 # 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 文档。Eli Bendersky 写的[这篇博客文章](http://eli.thegreenplace.net/2011/06/24/django-sessions-part-i-cookies/ "这篇博客文章")也值得一读。 # 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 是否存在于客户端电脑中。 ➋ 如果目标 cookie 存在,便可以读取其值:request.COOKIES[]。COOKIES 属性的值是个字 典,因此想读取 cookie 时,把 cookie 的名称(字符串形式)传入方括号中。记住,cookie 的值始终为字符串,不管存储的值是什么语义。因此,要做好转换类型的准备(例如使用 int() 或 float())。 ➌ 如果目标 cookie 不存在,或者想更新 cookie,把想保存的值传给生成的响应。要调用的函 数是 response.set_cookie('
', value),第一个参数是 cookie 的名称,第二 个参数是要设定的值。 如果对安全要求更高,应该使用基于会话的 cookie。 ➊ 首先,确保 Django 项目的 settings.py 模块中的 MIDDLEWARE_CLASSES 列表里有django.contrib.sessions.middleware.SessionMiddleware。如果没有,自行加上。 ➋ 使用 SESSION_ENGINE 配置会话后端。不同的后端配置参见 Django 文档。 ➌ 通过 requests.sessions.get() 检查 cookie 是否存在。 ➍ 通过会话字典更新或设定 cookie:requests.session['
']。