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
NeteaseUrl/core/session.py
T
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

74 lines
3.1 KiB
Python
Raw 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 -*-
"""会话 Cookie 的编解码(itsdangerous 签名格式,与 Flask 会话同格式)。
存量兼容:本站早期由 Flask 签发会话(`session` cookie,格式
`payload.timestamp.hmac`;盐 `cookie-session`;HMAC-SHA1;密钥派生 `hmac`),
为让**存量 Cookie 无需重新登录即可继续使用**,此处直接调用 itsdangerous——
与 Flask `SecureCookieSessionInterface.get_signing_serializer` 使用同一套参数,
格式逐字节一致,不再手写复刻签名算法(手写密码学原语是负债:难审、易错、
且必须靠测试钉住与上游的字节级一致)。
密钥同源:取自 auth_system.get_secret_key()(data/secret.key)。
"""
import hashlib
from functools import lru_cache
from typing import Any
from itsdangerous import BadData, URLSafeTimedSerializer
# 与 Flask 默认一致:SHA1 + itsdangerous 的 key derivation(HMAC(digest, salt))
_SALT = 'cookie-session'
_SIGNER_KWARGS: dict[str, Any] = {'key_derivation': 'hmac', 'digest_method': hashlib.sha1}
SESSION_KEY = 'site_uid'
@lru_cache(maxsize=8)
def _serializer(secret: str) -> URLSafeTimedSerializer:
"""构造与 Flask 会话同参数的序列化器(盐 / 派生 / 摘要三项对齐)。
按密钥缓存:`loads` 位于每个请求的会话解析路径上,每次新建序列化器是
无谓开销;密钥种类极少(通常只有 data/secret.key 一个),maxsize=8 足够。
"""
return URLSafeTimedSerializer(secret, salt=_SALT, signer_kwargs=dict(_SIGNER_KWARGS))
def dumps(secret: str, session: dict[str, Any]) -> str:
"""把会话字典序列化为 Flask/itsdangerous 兼容的签名 Cookie 值。"""
return _serializer(secret).dumps(session)
def loads(secret: str, value: str, max_age: int | None = None) -> dict[str, Any] | None:
"""校验签名并解出会话字典;无效 / 被篡改 / 超龄返回 None。
``max_age=None``(默认)不校验签发时间——与本站既有行为一致:会话有效期由
Cookie 自身的 max_age(登录时 7 天,见 web.routers.register._SESSION_MAX_AGE)
与库中 login_seq 顶号校验共同约束,服务端不叠加第二层独立过期判定;
传入秒数即启用 itsdangerous 的签名时间戳校验。
"""
if not value:
return None
try:
data = _serializer(secret).loads(value, max_age=max_age)
except BadData:
# BadSignature / SignatureExpired / BadPayload —— 一律视为无效会话
return None
return data if isinstance(data, dict) else None
def get_uid(session: dict[str, Any] | None) -> int | None:
"""取会话中的站点用户 id(Flask 存的是 int,直接可用)。
非整数一律按未登录处理:签名有效但字段畸形只可能来自密钥泄露后的伪造,
此时返回 None(当普通未登录)比抛 ValueError 触发 500 更安全。
"""
if not session:
return None
val = session.get(SESSION_KEY)
if not val:
return None
try:
return int(val)
except (TypeError, ValueError):
return None