feat: 集成 Keycloak 认证 — 前后端 + K8s 配置
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Keycloak JWT authentication middleware."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from jose import JWTError, jwt
|
||||
from jose.constants import Algorithms
|
||||
|
||||
load_dotenv(Path(__file__).parent.parent / ".env", override=True)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
KEYCLOAK_SERVER = os.getenv("KEYCLOAK_SERVER_URL", "https://keycloak.plfai.cn")
|
||||
KEYCLOAK_REALM = os.getenv("KEYCLOAK_REALM", "plfai")
|
||||
KEYCLOAK_AUDIENCE = os.getenv("KEYCLOAK_AUDIENCE", "account")
|
||||
|
||||
OIDC_CONFIG_URL = f"{KEYCLOAK_SERVER}/realms/{KEYCLOAK_REALM}/.well-known/openid-configuration"
|
||||
|
||||
# Cached OIDC config + JWKS
|
||||
_oidc_config: Optional[dict] = None
|
||||
_jwks: Optional[dict] = None
|
||||
|
||||
|
||||
async def _fetch_oidc_config() -> tuple[dict, dict]:
|
||||
"""Fetch OIDC config and JWKS, caching them."""
|
||||
global _oidc_config, _jwks
|
||||
|
||||
if _oidc_config is None:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(OIDC_CONFIG_URL)
|
||||
resp.raise_for_status()
|
||||
_oidc_config = resp.json()
|
||||
|
||||
jwks_resp = await client.get(_oidc_config["jwks_uri"])
|
||||
jwks_resp.raise_for_status()
|
||||
_jwks = jwks_resp.json()
|
||||
|
||||
return _oidc_config, _jwks # type: ignore[return-value]
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||||
) -> dict:
|
||||
"""Validate Bearer token and return user claims. Raises 401 if invalid."""
|
||||
if credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing Authorization header",
|
||||
)
|
||||
|
||||
token = credentials.credentials
|
||||
|
||||
try:
|
||||
_, jwks = await _fetch_oidc_config()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch OIDC config: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Auth service unavailable",
|
||||
)
|
||||
|
||||
# Get kid from token header
|
||||
try:
|
||||
unverified = jwt.get_unverified_header(token)
|
||||
except JWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token header",
|
||||
)
|
||||
|
||||
kid = unverified.get("kid")
|
||||
if not kid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing kid in token",
|
||||
)
|
||||
|
||||
# Find matching public key
|
||||
key = None
|
||||
for k in jwks.get("keys", []):
|
||||
if k.get("kid") == kid:
|
||||
key = k
|
||||
break
|
||||
|
||||
if key is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Unknown signing key",
|
||||
)
|
||||
|
||||
# Verify signature + expiry
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key,
|
||||
algorithms=[Algorithms.RS256],
|
||||
audience=KEYCLOAK_AUDIENCE,
|
||||
options={"verify_exp": True},
|
||||
)
|
||||
return payload
|
||||
except JWTError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Invalid token: {e}",
|
||||
)
|
||||
Reference in New Issue
Block a user