"""认证依赖:当前用户解析与状态变更防护。""" from typing import Annotated from fastapi import Depends, Header, HTTPException, Request, status from sqlalchemy.ext.asyncio import AsyncSession from app.auth.service import SESSION_COOKIE, resolve_session from app.db.session import get_db from app.models import User ALLOWED_ORIGINS: set[str] = set() # 由配置注入;空集表示本地开发放行同源 async def get_current_user( request: Request, db: Annotated[AsyncSession, Depends(get_db)], ) -> User: token = request.cookies.get(SESSION_COOKIE) if token is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "未登录") session = await resolve_session(db, token) if session is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "会话无效或已过期") user = await db.get(User, session.user_id) if user is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "会话主体不存在") # 请求级上下文供后续依赖使用(不重复查询) request.state.session_id = session.id request.state.session_csrf_hash = session.csrf_token_hash return user async def require_state_change( request: Request, user: Annotated[User, Depends(get_current_user)], hunjing_csrf_token: Annotated[str | None, Header(alias="Hunjing-CSRF-Token")] = None, ) -> User: """Cookie 状态变更:Origin 校验 + CSRF 双提交(契约 2.1 节)。""" if ALLOWED_ORIGINS: origin = request.headers.get("Origin") if origin is not None and origin not in ALLOWED_ORIGINS: raise HTTPException(status.HTTP_403_FORBIDDEN, "Origin 不被允许") csrf_hash = getattr(request.state, "session_csrf_hash", None) if csrf_hash is None or not _csrf_matches(csrf_hash, hunjing_csrf_token): raise HTTPException(status.HTTP_403_FORBIDDEN, "CSRF 校验失败") return user def _csrf_matches(expected_hash: str, provided: str | None) -> bool: import hashlib import secrets if provided is None: return False return secrets.compare_digest(expected_hash, hashlib.sha256(provided.encode()).hexdigest())