This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
snowy-yang 2b4704183c refactor: session 改用 itsdangerous,去掉手写密码学原语
为什么: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 拒绝。
2026-09-26 18:53:47 +08:00

100 lines
3.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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