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 1973d6a0e8 fix: 启用会话过期(服务端校验签发时间,堵住旧 Cookie 永不过期)
为什么:签发侧 Cookie 的 max_age 是 7 天,但那只是**浏览器**行为——被保存、
复制或从日志里捞出来的旧 Cookie 仍带着有效签名,而解析侧 loads 从不校验签发
时间(max_age=None),等于「登录态永不过期」。登出/顶号靠递增 login_seq 兜底,
但拿不到库的一方(如泄露的 Cookie)无从失效。

改法:
- core/session.py 新增 COOKIE_NAME / SESSION_MAX_AGE,作为会话名与有效期的
  唯一来源。此前 'session' 在 web/deps、web/routers/auth、web/routers/register
  各写一遍,7 天在两处各写一遍——三处同名常量各改各的,正是漂移温床。
- web/deps._parse_cookie 传入 max_age=SESSION_MAX_AGE,过期判定落在服务端。
- 两个签发侧改引用共享常量(保留模块级别名,对外名字不变)。
- loads() 的签名与语义**不变**(max_age=None 仍不校验):它是通用工具,策略
  属于调用点,这样 test_session 里「None 接受旧时间戳」的语义继续成立。

行为变更(本轮唯一一处):签发时间超过 7 天的会话 Cookie 现在按未登录处理。
正常浏览器用户无感——浏览器本来就按 max_age 丢弃它;受影响的是手工保存、
跨 7 天复用的旧 Cookie,正是要堵的那类。

验证:
- 新增 tests/test_deps.py::test_expired_cookie_rejected:用 monkeypatch.context()
  把时钟拨回 SESSION_MAX_AGE 之前签发(itsdangerous 时间戳取自 dumps 时刻),
  再以当前时间解析 → 必须拒绝;同时断言有效期内不受影响。用 context 而非
  undo(),后者会连带撤销 site fixture 的设置。
- 变异验证:把 max_age 参数摘掉后**恰好**该用例变红、其余 10 例全绿——证明测试
  精确盯着这一处,不是碰巧通过。
- ruff 全过 · pyright 0/0 · pytest 214 passed(213→214)。golden 快照与
  test_download_billing(真实登录拿 Cookie 打 API)均通过,端到端链路未受影响。
2026-09-27 07:32:05 +08:00

80 lines
3.4 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'
# 会话 Cookie 名与有效期(秒):签发侧(登录写 Cookie)与解析侧(校验签发时间)
# 共用这一处定义,避免两处各写一个 7 天、日后慢慢漂移。
COOKIE_NAME = 'session'
SESSION_MAX_AGE = 7 * 86400
@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``(默认)不校验签发时间。**生产请求路径
由 web.deps._parse_cookie 传入 SESSION_MAX_AGE 启用过期**——只靠 Cookie 的
max_age 是不够的:那只是浏览器行为,被保存、复制或从日志里捞出来的旧 Cookie
依然能通过签名校验,服务端不判就等于登录态永不过期。传入秒数即启用
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