Files
v6ole 848f804169 PingWatch 网络设备离线监控系统
- FastAPI 后端 + Vue 3 前端
- Docker Compose 一键部署
- Casdoor OAuth 认证集成
- LogHive 集中式日志
- 设备批量 CSV 导入/导出
- WebSocket 实时状态推送
- 企业微信告警通知
- fping 高性能并发 Ping 检测

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:02:04 +08:00

129 lines
4.6 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, verify=False) as client:
try:
resp = await client.post(token_url, data=data)
if resp.status_code != 200:
logger.error(f"Casdoor token 换取失败: {resp.status_code} {resp.text}")
return None
token_data = resp.json()
id_token = token_data.get("id_token")
if not id_token:
logger.error("Casdoor 返回中没有 id_token")
return None
# 解码 id_token (JWT) payload,不验证签名(HTTPS 已保证传输安全)
# 生产环境建议验证 Casdoor 证书
cert = _load_casdoor_certificate()
try:
payload = jwt.decode(
id_token,
key=cert or None,
options={"verify_signature": bool(cert)},
audience=settings.CASDOOR_CLIENT_ID,
)
except JWTError:
# 不验证签名的方式解码
payload = jwt.get_unverified_claims(id_token)
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