| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- """API 文档(Swagger UI / ReDoc)的访问保护。
- 默认 FastAPI 暴露 docs_url / redoc_url / openapi_url 是公开的。
- 本模块关闭这些公开路由,改为注册受 HTTP Basic 认证保护的等价路由。
- 用法(在 app/main.py 中):
- from app.docs_auth import setup_protected_docs
- app = FastAPI(
- ...
- docs_url=None,
- redoc_url=None,
- openapi_url=None,
- )
- setup_protected_docs(app)
- """
- from __future__ import annotations
- import secrets
- from fastapi import Depends, FastAPI, HTTPException, status
- from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
- from fastapi.security import HTTPBasic, HTTPBasicCredentials
- from app.config import settings
- # auto_error=False 让我们自己处理无凭据的情况,
- # 否则 FastAPI 抛出的 401 不带 WWW-Authenticate 头,浏览器不会弹登录框
- _security = HTTPBasic(auto_error=False)
- # 受保护后的实际访问路径(修改这里同时改变所有三个文档路径)
- DOCS_URL = "/faq/docs"
- REDOC_URL = "/faq/redoc"
- OPENAPI_URL = "/faq/openapi.json"
- def verify_docs_auth(
- credentials: HTTPBasicCredentials | None = Depends(_security),
- ) -> str:
- """校验 HTTP Basic 凭据。
- 无凭据 / 凭据错误时统一返回 401 + WWW-Authenticate: Basic,
- 浏览器看到此头会弹出登录框(首次访问 / 凭据错误均如此)。
- 使用 secrets.compare_digest 进行常量时间比较,防止时序攻击。
- """
- if credentials is None:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="需要 API 文档访问凭据",
- headers={"WWW-Authenticate": "Basic"},
- )
- expected_user = settings.docs_username.encode("utf-8")
- expected_pass = settings.docs_password.encode("utf-8")
- actual_user = credentials.username.encode("utf-8")
- actual_pass = credentials.password.encode("utf-8")
- user_ok = secrets.compare_digest(actual_user, expected_user)
- pass_ok = secrets.compare_digest(actual_pass, expected_pass)
- if not (user_ok and pass_ok):
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="API 文档访问凭据错误",
- headers={"WWW-Authenticate": "Basic"},
- )
- return credentials.username
- def setup_protected_docs(app: FastAPI) -> None:
- """注册受 HTTP Basic 认证保护的 Swagger / ReDoc / OpenAPI 路由。
- 必须在 FastAPI 实例化时关闭默认 docs(docs_url=None / redoc_url=None / openapi_url=None),
- 否则会与本模块注册的同名路径冲突。
- 本函数应在 setup_custom_openapi 之后调用,确保读取到的 schema 是覆写过的。
- """
- @app.get(OPENAPI_URL, include_in_schema=False)
- async def protected_openapi(
- _user: str = Depends(verify_docs_auth),
- ) -> dict:
- return app.openapi()
- @app.get(DOCS_URL, include_in_schema=False)
- async def protected_swagger(
- _user: str = Depends(verify_docs_auth),
- ):
- return get_swagger_ui_html(
- openapi_url=OPENAPI_URL,
- title=f"{app.title} - Swagger UI",
- )
- @app.get(REDOC_URL, include_in_schema=False)
- async def protected_redoc(
- _user: str = Depends(verify_docs_auth),
- ):
- return get_redoc_html(
- openapi_url=OPENAPI_URL,
- title=f"{app.title} - ReDoc",
- )
|