Django 操作 Cookie 和 Session
上一节介绍了 Cookie 和 Session 的相关概念,本节就要在 Django 中操作 Cookie 和 Session,同时我也会继续带领大家追踪相关的代码,这样可以更好的理解相关操作。
1. Django 中操作 Cookie
操作 Cookie 同样是考察4个基本动作:增删改查。现在分别从这4个角度看 Django 如何操作 Cookie :
增:对于视图函数或者视图类的三种返回 Response 响应 (HttpResponse、render、redircet),之前的做法是直接 return,现在可以在 return 之前,使用 set_cookie()
或者 set_signed_cookied()
方法给客户端颁发一个 cookie,然后再带着颁发的 cookie 响应用户请求。操作代码结构如下。
def xxxx(request, *args, **kwargs):
# ...
rep = HttpResponse(...)
# 或者
rep = render(request, ...)
# 或者
rep = redirect( ...)
# 两种设置cookie的方法,一种不加salt,另一个加salt
rep.set_cookie(key, value,...)
rep.set_signed_cookie(key, value, salt='加密盐', max_age=None, ...)
return rep
查:查询 cookie 是在发送过来的 HTTP 请求中的,因此对应的查询 Cookie 方法封装在 HttpRequest 类中,对应的操作语句如下:
request.COOKIES['key']
request.COOKIES.get['key']
# 对应前面使用前面加密的cookie
request.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None)
改:调用前面的 set_cookie()
或者 set_signed_cookie()
方法修改 Cookie 即可;
删:直接使用 HttpReponse 类的 delete_cookie()
删除 cookie 中对应 key 值。
案例1:Django 中 Cookie 实操。我们在前面的登录表单功能上改造视图函数,保证一次登录后,后续再次 GET 请求时能自动识别登录用户。此外还设置一个 Cookie 过期时间,过期之后再次 GET 请求时又回到登录页面。
调整登录表单的视图类:
class TestFormView2(TemplateView):
template_name = 'test_form2.html'
def get(self, request, *args, **kwargs):
success = False
form = LoginForm()
print("[{}] cookies:{}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), request.COOKIES))
if request.get_signed_cookie('user', default='anonymous', salt=default_salt) == 'spyinx':
success = True
return self.render_to_response(context={'success': success, 'form': form})
def post(self, request, *args, **kwargs):
form = LoginForm(request.POST)
success = True
err_msg = ""
rep = self.render_to_response(context={'success': success, 'err_msg': err_msg, 'form': form})
if form.is_valid():
login_data = form.clean()
name = login_data['name']
password = login_data['password']
if name != 'spyinx' or password != 'SPYinx123456':
success = False
err_msg = "用户名密码不正确"
else:
print('设置cookie')
rep.set_signed_cookie('user', 'spyinx', salt=default_salt, max_age=10)
else:
success = False
err_msg = form.errors['password'][0]
return rep
可以看到,在 get()
方法中我们通过 get_signed_cookie()
方法获取 cookie 中的 user
信息,判断是否为 spyinx。若正确则返回成功登录的页面,否则返回登录页面。在 post()
方法中,对于登录成功的情况我们通过 set_signed_cookie()
方法颁发了一个 cookie 给客户端,并设置过期时间为10s,后续客户端的请求中都会自动带上这个 cookie。
2. Django 中操作 Session
2.1 Session 的相关配置
由于 Session 的数据是保存在服务器端的,所以很多工作是需要在服务器端来完成的,所以 Django 中 Session 的操作相比 Cookie 操作会略显复杂。首先需要介绍 Django 中和 Session 相关的配置,同样是在 settings.py 文件中:
启用 Session:需要在 MIDDLEWARE
值中添加相应的 Session 中间件,去对 Session 拦截和处理。另外,还需要再INSTALLED_APPS
中注册 Session 应用。Django 中默认是有这个配置的。
MIDDLEWARE = [
# ...
'django.contrib.sessions.middleware.SessionMiddleware',
# ...
]
INSTALLED_APPS = [
# 启用 sessions 应用
'django.contrib.sessions',
]
配置 Session 引擎:主要是配置 Session 保存方式,比如数据库保存、内存保存、文件系统保存等。
# 数据库Session
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 默认引擎
# 缓存Session
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# 使用的缓存别名(默认内存缓存,也可以是memcache),此处别名依赖缓存的设置
SESSION_CACHE_ALIAS = 'default'
# 文件Session
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
# 缓存文件路径,如果为None,则使用tempfile模块获取一个临时地址tempfile.gettempdir()
SESSION_FILE_PATH = None
# 缓存+数据库
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
# 加密Cookie Session
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
其他配置如下:
# Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
SESSION_COOKIE_NAME = "sessionid"
# Session的cookie保存的路径(默认)
SESSION_COOKIE_PATH = "/"
# Session的cookie保存的域名(默认)
SESSION_COOKIE_DOMAIN = None
# 是否Https传输cookie(默认)
SESSION_COOKIE_SECURE = False
# 是否Session的cookie只支持http传输(默认)
SESSION_COOKIE_HTTPONLY = True
# Session的cookie失效日期(2周)(默认)
SESSION_COOKIE_AGE = 1209600
# 是否关闭浏览器使得Session过期(默认)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
# 是否每次请求都保存Session,默认修改之后才保存(默认)
SESSION_SAVE_EVERY_REQUEST = False
关于上述这些未出现在 settings.py 中的配置,默认的值都会在 django/conf/global_settings.py
中找到,如下图所示:
2.2 操作 Session
在 Django 中,如果我们配置好了 Session 中间件并注册了 Session应用 ,那么任何视图函数的第一个参数,也就是 HttpRequest
对象,将会包含一个 session
属性,我们可以在视图函数的任何位置使用 request.session
读取和写 Session 信息。这个 session
属性是前面配置的 SESSION_ENGINE 模块中 SessionStore
类的实例,而SessionStore
类又继承自 django.contrib.sessions.backends.base.SessionBase
类,它的具有如下操作方法:
- get(self, key, default=None):获取 Session 中的 key 值对应的 value。这种方式比较推荐。因为在 key 不存在时可以取默认值,而
request.session[key]
这样的写法在 key 不存在时会抛出异常; - pop(key, default=__not_given):返回 Session 中 key 对应的 value 值,并将其从 Session 中删除;
- keys():返回 Session 中所有的 key 值
- setdefault(key, value):给某个 key 设置对应的默认 value
- set_expiry(value):给 Session 设置对应的过期时间;
还有很多的方法可以在源码中找到,我们会在第3部分详细介绍这些代码。可以看到,目前我们操作 Session 就像操作字典那样简单,只需要使用 SessionBase
提供的各种方法即可随意操作 Session,这些都是 Django 在背后给我们做了许多工作,我们也会在第3部分详细介绍这些工作。
实验2:使用 Session 完成一个简单的登录操作,和上面 Cookie 实验类似。我们的模板文件不变,同样只需要改造下视图函数即可。
改造视图函数,使用 Session 保存登录信息
class TestFormView2(TemplateView):
template_name = 'test_form2.html'
def get(self, request, *args, **kwargs):
success = False
form = LoginForm()
if request.session.get('has_login', False):
return HttpResponse('已经登陆成功,无需再次登陆')
return self.render_to_response(context={'success': success, 'form': form})
def post(self, request, *args, **kwargs):
form = LoginForm(request.POST)
success = True
err_msg = ""
if form.is_valid():
login_data = form.clean()
name = login_data['name']
password = login_data['password']
if name != 'spyinx' or password != 'SPYinx123456':
success = False
err_msg = "用户名密码不正确"
else:
print('设置session')
request.session['has_login'] = True
# 设置10s后过期
request.session.set_expiry(10)
else:
success = False
err_msg = form.errors['password'][0]
return self.render_to_response(context={'success': success, 'err_msg': err_msg, 'form': form})
我们运行服务,来看相应的演示效果:
上面也可以看到,这里实现了和之前 Cookie 一样的效果。不过不同的是,在 Django 中我们默认的使用数据库的方式保存 Session 数据,这种方式在用户量大时频繁读写数据库会拖累 Web 服务的响应时间。
3. Django 中操作 Cookie 和 Session 的源码分析
3.1 Django 中 Cookie 操作相关源码
从前面的操作 Cookie 讲解中,我们只用到了和增和查两部分的方法,分别对应 HttpResponse 和 HttpRequest 两个类。接下来,我们去对应的源码中查找所涉及的和 Cookie 相关的代码。
request.COOKIES['xxx']
request.COOKIES.get('xxx', None)
# 源码位置:django/core/handlers/wsgi.py
class WSGIRequest(HttpRequest):
# ...
@cached_property
def COOKIES(self):
raw_cookie = get_str_from_wsgi(self.environ, 'HTTP_COOKIE', '')
return parse_cookie(raw_cookie)
# ...
# 源码位置:django/http/cookie.py
from http import cookies
# For backwards compatibility in Django 2.1.
SimpleCookie = cookies.SimpleCookie
# Add support for the SameSite attribute (obsolete when PY37 is unsupported).
cookies.Morsel._reserved.setdefault('samesite', 'SameSite')
def parse_cookie(cookie):
"""
Return a dictionary parsed from a `Cookie:` header string.
"""
cookiedict = {}
for chunk in cookie.split(';'):
if '=' in chunk:
key, val = chunk.split('=', 1)
else:
# Assume an empty name per
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
key, val = '', chunk
key, val = key.strip(), val.strip()
if key or val:
# unquote using Python's algorithm.
cookiedict[key] = cookies._unquote(val)
return cookiedict
上面的代码并不复杂,在 WSGIRequest
类中的 COOKIES 属性是先从客户端请求中取出 Cookie 信息,调用 get_str_from_wsgi()
方法是从 WSGI 中拿到对应的 Cookie 字符串。接下来用 parse_cookie()
方法将原始 Cookie 字符串中的 key=value
解析出来做成字典形式并返回。这就是为什么我们能像操作字典一样操作 request.COOKIES
的原因。下面的方法是实验1中调用的 get_signed_cookie()
的源码,也不复杂,同样是从self.COOKIES
中取出对应 key 的 value 值,然后使用对应的 salt 解密即可。
# 源码位置:django/http/request.py
class HttpRequest:
# ...
def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None):
"""
Attempt to return a signed cookie. If the signature fails or the
cookie has expired, raise an exception, unless the `default` argument
is provided, in which case return that value.
"""
try:
cookie_value = self.COOKIES[key]
except KeyError:
if default is not RAISE_ERROR:
return default
else:
raise
try:
value = signing.get_cookie_signer(salt=key + salt).unsign(
cookie_value, max_age=max_age)
except signing.BadSignature:
if default is not RAISE_ERROR:
return default
else:
raise
return value
# ...
接下来是涉及到创建 Cookie 的方法,我们需要查找 HttpResponse 类或者相关的父类:
# 源码位置:django/http/response.py
class HttpResponseBase:
# ...
def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
domain=None, secure=False, httponly=False, samesite=None):
"""
Set a cookie.
``expires`` can be:
- a string in the correct format,
- a naive ``datetime.datetime`` object in UTC,
- an aware ``datetime.datetime`` object in any time zone.
If it is a ``datetime.datetime`` object then calculate ``max_age``.
"""
self.cookies[key] = value
if expires is not None:
if isinstance(expires, datetime.datetime):
if timezone.is_aware(expires):
expires = timezone.make_naive(expires, timezone.utc)
delta = expires - expires.utcnow()
# Add one second so the date matches exactly (a fraction of
# time gets lost between converting to a timedelta and
# then the date string).
delta = delta + datetime.timedelta(seconds=1)
# Just set max_age - the max_age logic will set expires.
expires = None
max_age = max(0, delta.days * 86400 + delta.seconds)
else:
self.cookies[key]['expires'] = expires
else:
self.cookies[key]['expires'] = ''
if max_age is not None:
self.cookies[key]['max-age'] = max_age
# IE requires expires, so set it if hasn't been already.
if not expires:
self.cookies[key]['expires'] = http_date(time.time() + max_age)
if path is not None:
self.cookies[key]['path'] = path
if domain is not None:
self.cookies[key]['domain'] = domain
if secure:
self.cookies[key]['secure'] = True
if httponly:
self.cookies[key]['httponly'] = True
if samesite:
if samesite.lower() not in ('lax', 'strict'):
raise ValueError('samesite must be "lax" or "strict".')
self.cookies[key]['samesite'] = samesite
def set_signed_cookie(self, key, value, salt='', **kwargs):
value = signing.get_cookie_signer(salt=key + salt).sign(value)
return self.set_cookie(key, value, **kwargs)
def delete_cookie(self, key, path='/', domain=None):
# Most browsers ignore the Set-Cookie header if the cookie name starts
# with __Host- or __Secure- and the cookie doesn't use the secure flag.
secure = key.startswith(('__Secure-', '__Host-'))
self.set_cookie(
key, max_age=0, path=path, domain=domain, secure=secure,
expires='Thu, 01 Jan 1970 00:00:00 GMT',
)
# ...
从上面的代码可以看到,最核心的方法是 set_cookie()
,而删除 cookie 和 设置加盐的 cookie 方法最后都是调用 set_cookie()
这个方法。而这个方法也比较简单,就是将对应的传递过来的参数值加到 self.cookies
这个字典中。最后我们思考下,难道就这样就完了吗?是不是还需要有一步是需要将 self.cookies
中的所有 key-value 值组成字符串,放到头部中,然后才返回给前端?事实上,肯定是有这一步的,代码如下。在用 “#” 号包围起来的那一段代码正是将 self.cookies
中的所有 key-value 值组成字符串形式,然后放到头部的 “Set-Cookie” 中,正是有了这一步的动作,我们前面设置的 self.cookie
内部的 key-value 值才能真正生效。
# 源码位置:django/core/handlers/wsgi.py
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_middleware()
def __call__(self, environ, start_response):
set_script_prefix(get_script_name(environ))
signals.request_started.send(sender=self.__class__, environ=environ)
request = self.request_class(environ)
response = self.get_response(request)
response._handler_class = self.__class__
status = '%d %s' % (response.status_code, response.reason_phrase)
##############################################################################
response_headers = [
*response.items(),
*(('Set-Cookie', c.output(header='')) for c in response.cookies.values()),
]
#############################################################################
start_response(status, response_headers)
if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
response = environ['wsgi.file_wrapper'](response.file_to_stream)
return response
3.2 Django 中 Session 操作相关源码
Django 中和 Session 相关的代码较多,我们不去深入追究源码细节,主要是程序的执行过程。比如在哪里设置的 request.session
值以及 session 相关信息如何保存到数据库中的等等。我们先整体看下 Session 相关的代码位置:
这里有几个比较重要的地方,一个是 backends
目录,下面是不同保存 Session 数据方式的代码,如使用数据库存储、缓存存储或者文件系统存储等,每种存储方式对应着一个代码文件,里面定义的类和方法都是一致的,这样就可以无缝切换存储引擎。
第二个是 middleware.py
文件,我们先要了解下 Django 中 MIDDLEWARE 的工作流程,可以如下图所示:
由于在 settings.py 中间设置了 Session 的中间件,所以 request 和 response 也会经历 Session 中间件的这个流程,看 session 目录下的 middleware.py
文件的代码:
class SessionMiddleware(MiddlewareMixin):
def __init__(self, get_response=None):
self.get_response = get_response
engine = import_module(settings.SESSION_ENGINE)
self.SessionStore = engine.SessionStore
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)
def process_response(self, request, response):
"""
If request.session was modified, or if the configuration is to save the
session every time, save the changes and set a session cookie or delete
the session cookie if the session has been emptied.
"""
try:
accessed = request.session.accessed
modified = request.session.modified
empty = request.session.is_empty()
except AttributeError:
pass
else:
# First check if we need to delete this cookie.
# The session should be deleted only if the session is entirely empty
if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie(
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
domain=settings.SESSION_COOKIE_DOMAIN,
)
else:
if accessed:
patch_vary_headers(response, ('Cookie',))
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
if request.session.get_expire_at_browser_close():
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = http_date(expires_time)
# Save the session data and refresh the client cookie.
# Skip session save for 500 responses, refs #3881.
if response.status_code != 500:
try:
request.session.save()
except UpdateError:
raise SuspiciousOperation(
"The request's session was deleted before the "
"request completed. The user may have logged "
"out in a concurrent request, for example."
)
response.set_cookie(
# 默认值便是session_id
settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
samesite=settings.SESSION_COOKIE_SAMESITE,
)
return response
首先看 __init__()
方法中的设置了对应 Session 的存储引擎,接下来的 process_request()
方法中我们可以看到 request.session = self.SessionStore(session_key)
这行语句正是给 request 的 session 属性赋值,而这个值正是存储引擎模块下的 SessionStore
类的实例。而对于 process_response()
方法,从代码中可以看到,它完成了 Session 数据的保存以及将 session_id 值写到 cookie 中去并返回给客户端,调用的方法正是我们前面介绍到的 set_cookie()
方法。
接下来,我们看看其他几个 python 文件中的代码。例如下面的 models.py 定义了 Session 的 model 模型,包括字段以及管理器:
# 源码位置:django/contrib/sessions/models.py
from django.contrib.sessions.base_session import (
AbstractBaseSession, BaseSessionManager,
)
class SessionManager(BaseSessionManager):
use_in_migrations = True
class Session(AbstractBaseSession):
objects = SessionManager()
@classmethod
def get_session_store_class(cls):
from django.contrib.sessions.backends.db import SessionStore
return SessionStore
class Meta(AbstractBaseSession.Meta):
db_table = 'django_session'
从这段代码可以看到 Session 如果使用数据库保存数据的话,其建立的表名为:django_session,其字段类型并不在这里定义,而是继承的父类 BaseSessionManager
,这个类定义就在 base_session.py
文件中:
class BaseSessionManager(models.Manager):
def encode(self, session_dict):
"""
Return the given session dictionary serialized and encoded as a string.
"""
session_store_class = self.model.get_session_store_class()
return session_store_class().encode(session_dict)
def save(self, session_key, session_dict, expire_date):
s = self.model(session_key, self.encode(session_dict), expire_date)
if session_dict:
s.save()
else:
s.delete() # Clear sessions with no data.
return s
class AbstractBaseSession(models.Model):
session_key = models.CharField(_('session key'), max_length=40, primary_key=True)
session_data = models.TextField(_('session data'))
expire_date = models.DateTimeField(_('expire date'), db_index=True)
objects = BaseSessionManager()
class Meta:
abstract = True
verbose_name = _('session')
verbose_name_plural = _('sessions')
def __str__(self):
return self.session_key
@classmethod
def get_session_store_class(cls):
raise NotImplementedError
def get_decoded(self):
session_store_class = self.get_session_store_class()
return session_store_class().decode(self.session_data)
从这里就可以看到 django_session 表有3个字段,分别是session_key
、session_data
和 expire_date
。继承这个基类必须要实现 get_session_store_class()
这个方法。另外 app.py
用于指明 session 应用名称,exceptions.py
定义了两个简单的异常,serializers.py
的内容也比较简单,仅仅使用 pickle 模块封装了一个用于序列化的类:
import pickle
from django.core.signing import JSONSerializer as BaseJSONSerializer
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
protocol = pickle.HIGHEST_PROTOCOL
def dumps(self, obj):
return pickle.dumps(obj, self.protocol)
def loads(self, data):
return pickle.loads(data)
JSONSerializer = BaseJSONSerializer
我们也可以简单使用下这个类,其实就是熟悉 pickle 模块的 dumps()
和 loads()
方法。
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.sessions.serializers import PickleSerializer
>>> from hello_app.views import Member
>>> Member.objects.all().get(name='spyinx-1')
<Member: <spyinx-1, 18017715080>>
>>> m = Member.objects.all().get(name='spyinx-1')
>>> PickleSerializer().dumps(m)
b'\x80\x05\x95o\x01\x00\x00\x00\x00\x00\x00\x8c\x15django.db.models.base\x94\x8c\x0emodel_unpickle\x94\x93\x94\x8c\thello_app\x94\x8c\x06Member\x94\x86\x94\x85\x94R\x94}\x94(\x8c\x06_state\x94h\x00\x8c\nModelState\x94\x93\x94)\x81\x94}\x94(\x8c\x06adding\x94\x89\x8c\x02db\x94\x8c\x07default\x94ub\x8c\x02id\x94K\x05\x8c\x04name\x94\x8c\x08spyinx-1\x94\x8c\x03age\x94\x8c\x0225\x94\x8c\x03sex\x94K\x00\x8c\noccupation\x94\x8c\x07teacher\x94\x8c\tphone_num\x94\x8c\x0b18017715080\x94\x8c\x05email\x94\x8c\n221@qq.com\x94\x8c\x04city\x94\x8c\x08shenzhen\x94\x8c\rregister_date\x94\x8c\x08datetime\x94\x8c\x08datetime\x94\x93\x94C\n\x07\xe4\x04\t\x10\x18\n\x00\x00\x00\x94\x85\x94R\x94\x8c\x0cvip_level_id\x94N\x8c\x0f_django_version\x94\x8c\x062.2.11\x94ub.'
>>> m1 = PickleSerializer().loads(s)
>>> type(m1)
<class 'hello_app.models.Member'>
>>> m1.name
'spyinx-1'
最后,关于 Session 的引擎不用过多细究,里面默认用到的是 backends/db.py
。如果使用的是缓存引擎,代码内容也是大同小异的。主要认真研究两个类:
backends/base.py
中的SessionBase
类。这个是所有SessionStore
的基类,它具有的方法正是我们操作 session 的方法;backends/db.py
中的SessionStore
类。前面的request.session
便是该类的一个实例,它的代码内容也是不复杂的,主要针对该种存储方式需要完成特定的保存(save()
)、删除(delete()
) 以及导入(load()
)等方法。
到此为止,Django 中关于 Session 的代码就这么多了。剩下的代码细节还需要各位去慢慢专研,也希望大家能认真钻研 Django 的源码,多多思考,然后多多实践。
4. 小结
本小节中我们讲解了在 Django 中如何操作 Cookie 和 Session,并各给出了一个实战案例。接下来我们学习了 Django 中操作 Cookie 和 Session 的方法的源码,在熟悉了这些代码之后,对于我们操作 Cookie 和 Session 会更加得心应手 。