Archived
为什么:core/session.py 原本手写 HMAC + 自定义 base64 签名,等于自己复刻 itsdangerous 的核心功能——密码学原语自己实现是风险点,且难与上游的安全 修复同步。 关键约束(换库的坑):两者字节级格式并不一致——手写实现用标准 base64 且 保留 '=' 填充,itsdangerous 用 urlsafe 且去填充。若不钉住旧格式,部署瞬间 线上全部登录态会失效。 改法: - 用 URLSafeTimedSerializer,按 secret 做 lru_cache(maxsize=8),避免每请求 重建序列化器 - loads() 增加 max_age 参数(默认 None,保持既有「不叠加服务端过期」行为) - get_uid() 顺带加固:cookie 里塞非整数不再抛 ValueError 变 500,改为返回 None - 删除 _derive_key / _int_to_bytes 及手写 HMAC/b64 验证:新增 2 例回归测试,其中 legacy cookie 样本由替换前的旧实现在固定时间戳 1700000000 下采集,钉住的是格式而非实现;另一例验证 max_age=None 接受旧 时间戳、max_age=60 拒绝。
100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Flask 兼容会话编解码的单元测试。
|
||
|
||
验证 core.session 与 itsdangerous(Flask 会话所用库)**双向互通**:
|
||
Flask 时代签发的会话 Cookie 必须仍能被本站解析(用户不必重新登录)。
|
||
|
||
Flask 本身已从依赖中移除,这里直接用 itsdangerous 构造/校验
|
||
(Flask 会话即 itsdangerous URLSafeTimedSerializer:盐 cookie-session、HMAC-SHA1)。
|
||
"""
|
||
import hashlib
|
||
import time
|
||
|
||
import pytest
|
||
from itsdangerous import URLSafeTimedSerializer
|
||
|
||
from core import session as cs
|
||
|
||
SECRET = 'unit-test-secret-key'
|
||
|
||
|
||
def _flask_serializer():
|
||
"""等价于 Flask 的会话序列化器(同盐、同摘要算法、同 key 派生)。"""
|
||
return URLSafeTimedSerializer(
|
||
SECRET, salt='cookie-session',
|
||
signer_kwargs={'key_derivation': 'hmac', 'digest_method': hashlib.sha1})
|
||
|
||
|
||
def test_itsdangerous_to_core():
|
||
"""itsdangerous(Flask)签发的会话 Cookie 能被 core.session 解出。"""
|
||
cookie = _flask_serializer().dumps({'site_uid': 26, 'site_uid_seq': 5})
|
||
data = cs.loads(SECRET, cookie)
|
||
assert data == {'site_uid': 26, 'site_uid_seq': 5}
|
||
assert cs.get_uid(data) == 26
|
||
|
||
|
||
def test_core_to_itsdangerous():
|
||
"""core.session 签发的 Cookie 能被 itsdangerous 解出。"""
|
||
cookie = cs.dumps(SECRET, {'site_uid': 74, 'site_uid_seq': 8})
|
||
assert _flask_serializer().loads(cookie) == {'site_uid': 74, 'site_uid_seq': 8}
|
||
|
||
|
||
def test_core_timestamp_valid_for_timed_check():
|
||
"""时间戳编码与 itsdangerous 一致:max_age 校验可用(未过期)。"""
|
||
cookie = cs.dumps(SECRET, {'site_uid': 1})
|
||
assert _flask_serializer().loads(cookie, max_age=3600) == {'site_uid': 1}
|
||
|
||
|
||
def test_tampered_returns_none():
|
||
cookie = _flask_serializer().dumps({'site_uid': 26})
|
||
assert cs.loads(SECRET, cookie[:-3] + 'xyz') is None
|
||
|
||
|
||
def test_wrong_key_returns_none():
|
||
cookie = _flask_serializer().dumps({'site_uid': 26})
|
||
assert cs.loads('wrong-secret', cookie) is None
|
||
|
||
|
||
def test_garbage_returns_none():
|
||
assert cs.loads(SECRET, 'not-a-cookie') is None
|
||
assert cs.loads(SECRET, '') is None
|
||
|
||
|
||
def test_roundtrip_selfconsistency():
|
||
payload = {'site_uid': 9, 'site_uid_seq': 3, 'site_uid_ts': int(time.time())}
|
||
assert cs.loads(SECRET, cs.dumps(SECRET, payload)) == payload
|
||
|
||
|
||
def test_legacy_handwritten_cookie_still_loads():
|
||
"""旧手写实现签发的 Cookie 仍能被解析(存量会话不必重新登录)。
|
||
|
||
样本由「替换 itsdangerous 之前」的 core.session.dumps 生成,时间戳固定为
|
||
1700000000,故字符串永久有效。它钉住的是**格式**而非实现:旧实现的
|
||
payload 用标准 base64 且保留 `=` 填充,签名同理;itsdangerous 用 urlsafe
|
||
且去掉填充——两侧必须继续互通,否则线上所有登录态会在部署瞬间失效。
|
||
"""
|
||
legacy = ('eyJzaXRlX3VpZCI6MjYsInNpdGVfdWlkX3NlcSI6NX0='
|
||
'.ZVPxAA.hW2ZBQ0BL1OhI4D4eDKFFVvqGO0')
|
||
data = cs.loads(SECRET, legacy)
|
||
assert data == {'site_uid': 26, 'site_uid_seq': 5}
|
||
assert cs.get_uid(data) == 26
|
||
|
||
|
||
def test_max_age_none_accepts_old_timestamp():
|
||
"""默认不校验签发时间(max_age=None)——与既有行为一致,旧 Cookie 不过期。"""
|
||
legacy = ('eyJzaXRlX3VpZCI6MjYsInNpdGVfdWlkX3NlcSI6NX0='
|
||
'.ZVPxAA.hW2ZBQ0BL1OhI4D4eDKFFVvqGO0')
|
||
assert cs.loads(SECRET, legacy) is not None
|
||
# 显式给出 max_age 时,超龄的旧 Cookie 必须被拒绝(供未来启用过期用)
|
||
assert cs.loads(SECRET, legacy, max_age=60) is None
|
||
|
||
|
||
def test_get_uid_none():
|
||
assert cs.get_uid(None) is None
|
||
assert cs.get_uid({}) is None
|
||
|
||
|
||
@pytest.mark.parametrize('uid', [1, 26, 99999])
|
||
def test_various_uids(uid):
|
||
assert cs.get_uid(cs.loads(SECRET, cs.dumps(SECRET, {'site_uid': uid}))) == uid
|