Files
group_fqcd_jr/app/api/dependencies/auth.py
T

70 lines
2.9 KiB
Python
Raw Normal View History

2026-09-09 21:55:37 +08:00
from functools import lru_cache
from fastapi import Depends, Request
2026-09-09 21:55:37 +08:00
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.core.actor import is_visitor
2026-09-09 21:55:37 +08:00
from app.core.config import get_settings
from app.core.contracts import RequestContext
from app.core.errors import UnauthorizedAgentError
2026-09-09 21:55:37 +08:00
from app.core.security import JwtAuthenticator
from app.service.identity_service import IdentityService
_bearer = HTTPBearer(auto_error=False)
# 与 `app/api/middleware.py` 的 `TRACE_ID_HEADER` 同名;认证阶段还不存在请求上下文,
# 这个头是唯一可复用的追踪标识来源。
TRACE_ID_HEADER = "X-Trace-ID"
2026-09-09 21:55:37 +08:00
@lru_cache(maxsize=1)
def _authenticator() -> JwtAuthenticator:
return JwtAuthenticator(get_settings())
async def build_request_context(
request: Request,
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer), # noqa: B008
) -> RequestContext:
"""认证 + 身份解析。
失败一律抛 `UnauthorizedAgentError`(`401 AUTHENTICATION_REQUIRED`),由
`app/main.py` 的 `AgentError` 处理器输出文档 §3.4 的统一错误信封——此前这里抛
FastAPI 原生 `HTTPException`,响应体是 `{"detail": "Unauthorized"}`,调用方无法用
同一套信封解析。令牌非法与 RBAC/身份解析失败**仍不区分**,都返回 401。
`meta.trace_id` 的来源(文档 §3.4):此时请求上下文尚未建立,因此先记住请求头里的
`X-Trace-ID`;没有就是空字符串,不凭空生成 id。
"""
request.state.trace_id = request.headers.get(TRACE_ID_HEADER)
def unauthorized() -> UnauthorizedAgentError:
"""消息固定成一句,涵盖令牌缺失、非法、过期、吊销与账号不可用。
文档 §3.6 只给了一个 401 码,因此客户端无法(也不应)据 `message` 区分失败类型,
更不会看到内部异常原文。
"""
return UnauthorizedAgentError("令牌缺失、无效或已吊销")
2026-09-09 21:55:37 +08:00
if credentials is None or credentials.scheme.lower() != "bearer":
raise unauthorized()
2026-09-09 21:55:37 +08:00
try:
context = _authenticator().authenticate(credentials.credentials)
if not is_visitor(context):
2026-09-10 13:56:08 +08:00
context = await IdentityService().resolve(context)
2026-09-09 21:55:37 +08:00
except Exception as exc:
# 令牌非法、账号停用、角色读取失败一律按 401 处理,形态完全一致。
raise unauthorized() from exc
2026-09-09 21:55:37 +08:00
request.state.request_context = context
2026-09-13 15:56:54 +08:00
if (
"customer" in context.roles
and not request.url.path.startswith("/api/v1/onboarding/")
):
2026-09-11 12:57:07 +08:00
from app.service.risk_questionnaire_service import RiskQuestionnaireService
if await RiskQuestionnaireService().is_required(context):
from app.core.errors import OnboardingRequiredError
raise OnboardingRequiredError("请先完成开户风险测评问卷")
2026-09-09 21:55:37 +08:00
return context