"""认证依赖:当前用户解析与状态变更防护。""" 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 DbSession, 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, db: DbSession, 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 / ADR-021)。""" 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 校验失败") # 保全态:欠费只读保全,状态变更拒绝(数据不删、恢复即解锁) from sqlalchemy import select from app.models import PersonalDomain domain = ( await db.execute( select(PersonalDomain).where(PersonalDomain.user_id == user.id).limit(1) ) ).scalar_one_or_none() if domain is not None and domain.billing_status == "preserved": raise HTTPException( status.HTTP_403_FORBIDDEN, "账户处于保全态(只读):恢复付费后自动解锁,数据不会删除", ) 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())