Django session 系统的实现。
先看 Django 官方文档 理解其提供的功能。Session 系统实现细节很多,比如支持匿名 session 时,要考虑用户从匿名变登陆后,服务端 session 数据的保持等。因此先理解该系统提供的功能是必要的。
下面结合代码讲解一个请求到 Django 后的整个处理流程。
用户首次打开 Django 站点
此时用户请求中的 cookie 并不带 session ID(cookie key 为 sessionid
)。假如 Django 在处理此请求过程中并没有向 request.session
写入数据,那回包也不会下发 session ID cookie。
假如用户后续使服务端 Django 写入了 session 数据,比如用户在未登陆状态下将物品添加进了购物车,那么 Django 会生成 session ID 并通过 cookie 下发给用户。
如果用户后续登陆了,这些 session 数据会被保留。
功能划分
Session 中间件的代码位于 django.contrib.sessions.middleware
包,主要由两块组成:
middleware.py
:- 处理请求包中的 session cookie
- 根据 session 的属性(是否 browser session,过期时间多长等),向回包写入 session cookie,以及 Vary 头等
backends
包:- 安全相关的处理,比如校验 session 是否属于该 user,配合 auth 模块防范 session fixation 等
- 提供
request.session
供操作 session 数据 - 从持久性存储(backend)读取和写入
- 实现内置的 serializer 来序列化 session data
request.session
是一个 dict-like 对象,封装了对底层 session store 的调用,使用户并不用关心底层细节。比如用户将物品加入购物车,你可以这样写:
request.session['cart'].append({'sku': abc, 'quantity': 1})
请求带 Session Cookie 到后台
先通过 session middleware 的 process_request()
:
# django.contrib.sessions.middleware.SessionMiddleware.process_request
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)
之后框架对 request.session
的操作(比如 .get()
)均由 session store 处理。大多数的逻辑都在 SessionBase 中处理。下面的逻辑在 django.contrib.sessions.backends.base.SessionBase
中。
当你使用 request.session.get()
时:
def get(self, key, default=None):
return self._session.get(key, default)
self._session
是一个 lazy load 的属性,会调用 self.load()
:
def _get_session(self, no_load=False):
"""
Lazily load session from storage (unless "no_load" is True, when only
an empty dict is stored) and store it in the current instance.
"""
self.accessed = True
try:
return self._session_cache
except AttributeError:
if self.session_key is None or no_load:
self._session_cache = {}
else:
self._session_cache = self.load()
return self._session_cache
_session = property(_get_session)
由于存储跟具体的 SessionStore
有关,SessionStore
需要继承 SessionBase
,并实现这些方法:
def exists(self, session_key):
"""
Return True if the given session_key already exists.
"""
raise NotImplementedError('subclasses of SessionBase must provide an exists() method')
def create(self):
"""
Create a new session instance. Guaranteed to create a new object with
a unique key and will have saved the result once (with empty data)
before the method returns.
"""
raise NotImplementedError('subclasses of SessionBase must provide a create() method')
def save(self, must_create=False):
"""
Save the session data. If 'must_create' is True, create a new session
object (or raise CreateError). Otherwise, only update an existing
object and don't create one (raise UpdateError if needed).
"""
raise NotImplementedError('subclasses of SessionBase must provide a save() method')
def delete(self, session_key=None):
"""
Delete the session data under this key. If the key is None, use the
current session key value.
"""
raise NotImplementedError('subclasses of SessionBase must provide a delete() method')
def load(self):
"""
Load the session data and return a dictionary.
"""
raise NotImplementedError('subclasses of SessionBase must provide a load() method')
@classmethod
def clear_expired(cls):
"""
Remove expired sessions from the session store.
If this operation isn't possible on a given backend, it should raise
NotImplementedError. If it isn't necessary, because the backend has
a built-in expiration mechanism, it should be a no-op.
"""
raise NotImplementedError('This backend does not support clear_expired().')
一个用户 session 的数据结构是:
- Session key:用站点 SECRET 及用户 hash 过的密码再 hash 出来的字符串
- Session data:服务端存储的业务数据,即
request.session
操作的数据- 这个数据由落入存储时要再经过
SessionBase.encode()
,后面安全一节详谈
- 这个数据由落入存储时要再经过
- Expire age:读取及设置过期 时长(比如 30 天)的能力
过期时长在这些地方被使用:
- 具体的 session store 会使用:
- Database store 将过期时间存入数据库,用于未来判断该 session 是否仍然有效
- Redis store(非内置)可以用其设置 key 的 exipre time
- middleware.py 中为 cookie 设置有效时间
但具体的 session store 可能用 过期时长 或 过期时间 来落存储(.save()
),只要它们 .load()
时能转换成过期时长即可。比如数据库适合用过期时间,这样方便 SQL 做筛选;Redis 适合用过期时长,方便设置 expire。
安全
安全分几块:
- Session 存储相关
- 用户登陆态相关
Session 存储相关
上文提到 session 数据分 3 块:session key,session data 及 expire age。其中:
- Session key:
- 需要通过 cookie 给到前端
- 需要存储到 session store,使 store 可以通过此 key 筛选出 session data
- Django 使用随机生成的 32 位字符串实现
- Expire age 敏感度较低,一般无需特殊处理
Session data 单独拿出来讲。因为 Django 支持以 cookie 作为 session store,因此 session data 需要通过 cookie 在 client 与 server 间传输。为此 Django 对 session data 做了压缩、加了签名,而且不管是不是 cookie 作为 store 都会执行此逻辑。代码在 SessionBase
的 .encode()
及 .decode()
处。
encode()
的处理很复杂。下面 Django 3.2 的处理流程。最终生成的 signed_value
被写入 store:
# django.core.signing.Signer.sign_object
value = base64_encode(
compress(
serializer.dump(
session_data_dict
)
)
)
# django.core.signing.TimestampSigner.sign
value_with_timestamp = value + ":" + now_timestamp
# django.core.signing.Signer.signature
signature = base64_encode(
salted_hmac(
some_fixed_salt, value_with_timestamp, settings.SECRET as key, "sha256"
)
)
# django.core.signing.Signer.sign
signed_value = value_with_timestamp + ":" + signature
这套流程体现了 Django 的 设计理念,在官方文档这 一节 中的 WARNING 处描述。重点是:
- SECRET 作为盐保证了只有你的站点能生成合法的 signature
- 假如你用 DB 作为 store,而 DB 被攻破,那么:
- 黑客无法伪造 session data
- 即使黑客使用自己的 session data 覆盖别人的,也不起作用(下文详述)
- 假如你用 DB 作为 store,而 DB 被攻破,那么:
- Session 数据没有被加密
- 假如你用 DB 作为 store,而 DB 被攻破,那么 session data 可以被解出来,黑客还可以知道 session 属于哪个用户(下文详述)
用户登陆态相关
安全这块跟 auth 模块 紧密相关。登陆相关的实现入口在 django.contrib.auth.get_user
。
用户登录成功后,Django auth 模块会在 request.session
中写入 3 个数据,例如:
_auth_user_id | 1 |
---|---|
_auth_user_backend | django.contrib.auth.backends.ModelBackend |
_auth_user_hash | 23b67b81196ced88b99560372436906edfa5b3bf |
其中 _auth_user_hash
是以固定串加上站点 SECRET_KEY
为盐,对用户存储在数据库中的密码(哈希过的,见 Auth: Authentication: Password)生成的 HMAC 哈希值。代码实现在 django.contrib.auth.base_user.AbstractBaseUser.get_session_auth_hash
。
这个 hash 值存在的意义是:
- 登陆的用户每次有请求过来时,Django 会校验其 session 中的 hash 与 password 算出来的值是否一致;如果不一致,会将当前的 session 清除,
request.user
置 None,用户的登陆态即消失- 处理逻辑的入口在
django.contrib.auth.middleware.AuthenticationMiddleware.process_request
- 处理逻辑的入口在
- 用户在修改密码后,会将其原有的 session 清理并生成新的,然后就新的 hash 写入 session
- 结合上一点,修改密码后,用户的各 session 都会在下次请求时被清除
另外 auth 模块也处理 session fixation(详见 Session Management)。用户被黑客注入 session key 后,当用户发起登陆请求时,Django 会校验其 session key 关联的 _user_auth_hash
是否属于此用户(这是有可能的,比如未受攻击情况下,在未登录状态下同时打开了两个登录页面,其中一个成功登录,又在另一个上做登录时),如果不属于此用户,会把原有的 session 清理掉,再新建一个 session。这样做使得黑客无法与用户共享同个 session。