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/tests/test_deps.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

111 lines
3.8 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 -*-
"""FastAPI 鉴权依赖测试:会话解析、封禁/顶号拒绝、登录与管理员闸。"""
import sqlite3
import time
import pytest
import core.db as db
from core import session as cs
from web.deps import SiteUser, current_user, require_admin, require_login
from web.errors import AppError
SECRET = 'deps-test-secret'
@pytest.fixture()
def site(tmp_path, monkeypatch):
path = tmp_path / 'site.db'
conn = sqlite3.connect(str(path))
conn.executescript("""
CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, role TEXT DEFAULT 'user',
status INTEGER DEFAULT 1, login_seq INTEGER DEFAULT 0,
duration_seconds INTEGER DEFAULT 0, ban_reason TEXT DEFAULT '');
INSERT INTO users (id, username, role, status, login_seq, duration_seconds)
VALUES (1,'qfbsh','admin',1,376,-1),
(26,'normal','user',1,5,97),
(27,'banned','user',2,1,0),
(28,'disabled','user',0,1,0);
""")
conn.commit()
conn.close()
monkeypatch.setattr(db, 'DATA_DIR', tmp_path)
monkeypatch.setattr(db, 'DB_PATH', path)
monkeypatch.setattr('web.deps.secret_key', lambda: SECRET)
return path
def _cookie(uid, seq, ts=None):
return cs.dumps(SECRET, {'site_uid': uid, 'site_uid_seq': seq,
'site_uid_ts': ts or int(time.time())})
async def test_valid_user(site):
u = await current_user(_cookie(26, 5))
assert u is not None and u.id == 26 and u.username == 'normal'
assert not u.is_admin and not u.unlimited
async def test_admin_unlimited(site):
u = await current_user(_cookie(1, 376))
assert u is not None and u.is_admin and u.unlimited
async def test_missing_cookie(site):
assert await current_user(None) is None
async def test_banned_rejected(site):
assert await current_user(_cookie(27, 1)) is None
async def test_disabled_rejected(site):
assert await current_user(_cookie(28, 1)) is None
async def test_stale_login_seq_rejected(site):
"""顶号:会话版本与库不一致 → 视为未登录。"""
assert await current_user(_cookie(26, 4)) is None
async def test_forged_cookie_rejected(site):
assert await current_user('not-a-valid-cookie') is None
async def test_expired_cookie_rejected(site, monkeypatch):
"""超龄会话 Cookie 由**服务端**拒绝(不只是浏览器层面的 max_age)。
Cookie 的 max_age 只是浏览器行为:被保存/复制的旧 Cookie 仍带着有效签名,
服务端不校验签发时间就等于登录态永不过期。
构造方式:用 monkeypatch.context() 把时钟拨回 SESSION_MAX_AGE 之前签发
(itsdangerous 的签名时间戳取自 dumps 时刻),再以当前时间解析。
必须用 context 而非 monkeypatch.undo()——后者会连带撤销 site fixture 的设置。
"""
past = time.time() - cs.SESSION_MAX_AGE - 60
with monkeypatch.context() as m:
m.setattr(time, 'time', lambda: past)
stale = _cookie(26, 5)
assert await current_user(stale) is None, '超龄会话必须被服务端拒绝'
fresh = await current_user(_cookie(26, 5))
assert fresh is not None and fresh.id == 26, '有效期内不受影响'
async def test_require_login_raises(site):
with pytest.raises(AppError) as ei:
await require_login(None)
assert ei.value.status_code == 401
async def test_require_admin_forbidden_for_user(site):
u = SiteUser(id=26, username='normal', role='user', credits=97)
with pytest.raises(AppError) as ei:
await require_admin(u)
assert ei.value.status_code == 403
async def test_require_admin_ok(site):
u = SiteUser(id=1, username='qfbsh', role='admin', credits=-1)
assert (await require_admin(u)).is_admin