Files
yml2213 6fc453017c feat(auth): 后台登录改为滑动会话
- JWT 记录 iat,exp=min(now+24h, iat+7d)
- 已认证请求自动续签 httpOnly Cookie
- 新增 ACCESS_TOKEN_IDLE_HOURS / ACCESS_TOKEN_MAX_HOURS
2026-10-25 14:07:44 +08:00

88 lines
2.7 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.
"""后台登录 Cookie 与滑动续期中间件。
续期只发生在「已经通过 ``get_current_user`` 的请求」上:
- ``get_current_user`` 把当前用户和「本次认证用的 Cookie Token」写进
``request.state``;
- 响应完成后,中间件读取该 Token,把 ``exp`` 推到 ``now + 24h``(不超过 7 天);
- 登录流程、登出、未认证请求、401 响应一律不续期。
"""
from __future__ import annotations
import os
from datetime import UTC, datetime
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from .security import AccessToken, renew_access_token
SESSION_COOKIE_NAME = "access_token"
# 生产环境 HTTPS 部署时设置 COOKIE_SECURE=true
_COOKIE_SECURE = os.getenv("COOKIE_SECURE", "false").lower() == "true"
# 登出会删除 Cookie,不能再被中间件补回来。
_NO_RENEW_PATHS = frozenset({"/api/auth/logout"})
def set_session_cookie(
response: Response,
token: str,
expires_at: datetime,
*,
now: datetime | None = None,
) -> None:
"""写入会话 Cookie;有效期跟随 JWT 的绝对过期时刻。"""
current = now or datetime.now(UTC)
max_age = int((expires_at - current).total_seconds())
if max_age <= 0:
return
response.set_cookie(
key=SESSION_COOKIE_NAME,
value=token,
httponly=True,
secure=_COOKIE_SECURE,
samesite="lax",
max_age=max_age,
expires=expires_at,
path="/",
)
def clear_session_cookie(response: Response) -> None:
response.delete_cookie(key=SESSION_COOKIE_NAME, path="/")
def _should_renew(request: Request, status_code: int) -> bool:
"""只对「已认证的 API 请求 + 非 401 + 非登出」续期。"""
path = request.url.path.rstrip("/") or "/"
if status_code == 401:
return False
if not path.startswith("/api/"):
return False
if path in _NO_RENEW_PATHS:
return False
return getattr(request.state, "current_user", None) is not None
class SessionRenewalMiddleware(BaseHTTPMiddleware):
"""滑动续期:活跃请求自动延长 24h,但永不突破 7 天绝对上限。"""
async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
response = await call_next(request)
if not _should_renew(request, response.status_code):
return response
token = getattr(request.state, "auth_cookie_token", None)
if not token:
return response
renewed: AccessToken | None = renew_access_token(token)
if renewed is None:
return response
set_session_cookie(response, renewed.token, renewed.expires_at)
return response