Files
PingWatch/backend/app/core/auth.py
T

126 lines
4.3 KiB
Python

"""
Casdoor OAuth 认证集成 + JWT 会话管理
流程:
1. 前端跳转到 Casdoor 登录页 → 用户登录
2. Casdoor 回调到前端(带 code 参数)
3. 前端将 code 发到后端 /api/auth/login
4. 后端用 code 向 Casdoor 换取 access_token + id_token
5. 后端从 id_token (JWT) 解析用户信息
6. 后端签发自己的 JWT,返回给前端
"""
import logging
import os
from datetime import datetime, timedelta
from typing import Optional
import httpx
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.user import User, UserRoleEnum
from app.core.deps import get_db
logger = logging.getLogger("pingwatch.auth")
security = HTTPBearer()
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""签发 PingWatch 自己的 JWT"""
to_encode = data.copy()
expire = datetime.now() + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
def _load_casdoor_certificate() -> str:
"""加载 Casdoor 证书:优先读取 dahua.pem,否则用环境变量"""
pem_path = os.path.join(os.path.dirname(__file__), "..", "..", "dahua.pem")
pem_path = os.path.normpath(pem_path)
if os.path.isfile(pem_path):
with open(pem_path, "r") as f:
return f.read()
return settings.CASDOOR_CERTIFICATE
async def exchange_code_for_user(code: str) -> Optional[dict]:
"""
用 OAuth code 向 Casdoor 换取用户信息。
步骤:
1. POST → /api/login/oauth/access_token 换取 id_token
2. 解码 id_token (JWT) 得到用户信息
"""
token_url = f"{settings.CASDOOR_ENDPOINT}/api/login/oauth/access_token"
data = {
"grant_type": "authorization_code",
"client_id": settings.CASDOOR_CLIENT_ID,
"client_secret": settings.CASDOOR_CLIENT_SECRET,
"code": code,
}
async with httpx.AsyncClient(timeout=15) as client:
try:
resp = await client.post(token_url, data=data)
if resp.status_code != 200:
logger.error("Casdoor token 换取失败: status=%s", resp.status_code)
return None
token_data = resp.json()
id_token = token_data.get("id_token")
if not id_token:
logger.error("Casdoor 返回中没有 id_token")
return None
cert = _load_casdoor_certificate()
if not cert:
logger.error("Casdoor token 验证证书未配置")
return None
payload = jwt.decode(
id_token,
key=cert,
audience=settings.CASDOOR_CLIENT_ID,
)
return payload
except httpx.TimeoutException:
logger.error("Casdoor token 请求超时")
return None
except Exception as e:
logger.error(f"Casdoor token 请求异常: {e}", exc_info=True)
return None
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db),
) -> User:
"""从 PingWatch JWT 中解析当前登录用户"""
token = credentials.credentials
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
casdoor_uid: str = payload.get("sub", "")
if not casdoor_uid:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效 token")
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效 token")
result = await db.execute(select(User).where(User.casdoor_uid == casdoor_uid))
user = result.scalar_one_or_none()
if not user or not user.is_active:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在或已禁用")
return user
async def require_admin(current_user: User = Depends(get_current_user)) -> User:
if current_user.role != UserRoleEnum.admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可执行此操作")
return current_user