215 lines
7.4 KiB
Python
215 lines
7.4 KiB
Python
"""
|
|
Casdoor SSO 集成模块
|
|
==================
|
|
实现 Casdoor OIDC 单点登录集成,
|
|
支持 JWT + Casdoor 双认证模式。
|
|
|
|
使用方式:
|
|
from app.casdoor_auth import casdoor_router, get_casdoor_login_url, handle_casdoor_callback
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
import httpx
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
from urllib.parse import urlencode
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
|
|
from .database import get_db
|
|
from .models import User, gen_uuid
|
|
from .auth import create_access_token, hash_password
|
|
|
|
logger = logging.getLogger("aiflow.casdoor")
|
|
|
|
router = APIRouter()
|
|
|
|
# Casdoor 配置
|
|
CASDOOR_ENDPOINT = os.environ.get("CASDOOR_ENDPOINT", "http://casdoor:8000")
|
|
CASDOOR_CLIENT_ID = os.environ.get("CASDOOR_CLIENT_ID", "aiflow")
|
|
CASDOOR_CLIENT_SECRET = os.environ.get("CASDOOR_CLIENT_SECRET", "")
|
|
CASDOOR_ORG_NAME = os.environ.get("CASDOOR_ORG_NAME", "aiflow")
|
|
CASDOOR_APP_NAME = os.environ.get("CASDOOR_APP_NAME", "aiflow-app")
|
|
CASDOOR_REDIRECT_URI = os.environ.get(
|
|
"CASDOOR_REDIRECT_URI",
|
|
"http://210.16.161.218:12888/app/api/auth/casdoor/callback"
|
|
)
|
|
APP_FRONTEND_URL = os.environ.get("APP_ACCESS_URL", "http://210.16.161.218:12888")
|
|
|
|
|
|
def get_casdoor_login_url(state: str = "") -> str:
|
|
"""获取 Casdoor 登录跳转URL"""
|
|
params = {
|
|
"client_id": CASDOOR_CLIENT_ID,
|
|
"response_type": "code",
|
|
"redirect_uri": CASDOOR_REDIRECT_URI,
|
|
"scope": "openid profile email",
|
|
"state": state or "aiflow",
|
|
}
|
|
return f"{CASDOOR_ENDPOINT}/login/oauth/authorize?{urlencode(params)}"
|
|
|
|
|
|
async def exchange_code_for_token(code: str) -> Optional[dict]:
|
|
"""用授权码换取 access_token 和用户信息"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
# 获取 token
|
|
token_resp = await client.post(
|
|
f"{CASDOOR_ENDPOINT}/api/login/oauth/access_token",
|
|
json={
|
|
"grant_type": "authorization_code",
|
|
"client_id": CASDOOR_CLIENT_ID,
|
|
"client_secret": CASDOOR_CLIENT_SECRET,
|
|
"code": code,
|
|
"redirect_uri": CASDOOR_REDIRECT_URI,
|
|
},
|
|
)
|
|
|
|
if token_resp.status_code != 200:
|
|
logger.error(f"Casdoor token exchange failed: {token_resp.status_code} {token_resp.text}")
|
|
return None
|
|
|
|
token_data = token_resp.json()
|
|
access_token = token_data.get("access_token")
|
|
if not access_token:
|
|
logger.error("No access_token in Casdoor response")
|
|
return None
|
|
|
|
# 获取用户信息
|
|
user_resp = await client.get(
|
|
f"{CASDOOR_ENDPOINT}/api/account",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
|
|
if user_resp.status_code != 200:
|
|
# 尝试备用端点
|
|
user_resp = await client.get(
|
|
f"{CASDOOR_ENDPOINT}/api/userinfo",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
|
|
if user_resp.status_code != 200:
|
|
logger.error(f"Casdoor userinfo failed: {user_resp.status_code}")
|
|
return {"access_token": access_token, "user": None}
|
|
|
|
user_info = user_resp.json()
|
|
return {"access_token": access_token, "user": user_info}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Casdoor token exchange error: {e}")
|
|
return None
|
|
|
|
|
|
async def get_or_create_user(db: Session, casdoor_user: dict) -> User:
|
|
"""根据 Casdoor 用户信息获取或创建本地用户"""
|
|
casdoor_id = casdoor_user.get("id", casdoor_user.get("name", ""))
|
|
casdoor_name = casdoor_user.get("name", casdoor_user.get("preferred_username", ""))
|
|
casdoor_email = casdoor_user.get("email", "")
|
|
casdoor_display = casdoor_user.get("displayName", casdoor_user.get("name", ""))
|
|
|
|
# 通过 Casdoor ID 查找已有用户
|
|
existing = db.query(User).filter(User.casdoor_id == casdoor_id).first() if hasattr(User, "casdoor_id") else None
|
|
|
|
# 通过用户名查找
|
|
if not existing and casdoor_name:
|
|
existing = db.query(User).filter(User.username == f"sso_{casdoor_name}").first()
|
|
if existing and hasattr(existing, "casdoor_id"):
|
|
existing.casdoor_id = casdoor_id
|
|
|
|
if existing:
|
|
# 更新最后登录时间
|
|
existing.last_login = datetime.now()
|
|
db.commit()
|
|
return existing
|
|
|
|
# 创建新用户
|
|
username = f"sso_{casdoor_name}" if casdoor_name else f"sso_{casdoor_id[:8]}"
|
|
# 避免用户名冲突
|
|
base_username = username
|
|
counter = 1
|
|
while db.query(User).filter(User.username == username).first():
|
|
username = f"{base_username}_{counter}"
|
|
counter += 1
|
|
|
|
new_user = User(
|
|
id=gen_uuid(),
|
|
username=username,
|
|
email=casdoor_email or None,
|
|
password_hash=hash_password(casdoor_id + datetime.now().isoformat()), # 随机密码
|
|
role="user",
|
|
status="active",
|
|
display_name=casdoor_display or username,
|
|
)
|
|
|
|
# 如果 User 模型有 casdoor_id 字段则设置
|
|
if hasattr(new_user, "casdoor_id"):
|
|
new_user.casdoor_id = casdoor_id
|
|
|
|
db.add(new_user)
|
|
db.commit()
|
|
db.refresh(new_user)
|
|
|
|
logger.info(f"Created SSO user: {username} (casdoor_id={casdoor_id})")
|
|
return new_user
|
|
|
|
|
|
# ==================== API 路由 ====================
|
|
|
|
@router.get("/casdoor/login")
|
|
async def casdoor_login(state: str = ""):
|
|
"""跳转到 Casdoor 登录页面"""
|
|
login_url = get_casdoor_login_url(state)
|
|
return RedirectResponse(url=login_url)
|
|
|
|
|
|
@router.get("/casdoor/callback")
|
|
async def casdoor_callback(
|
|
code: str,
|
|
state: str = "",
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Casdoor OAuth 回调"""
|
|
result = await exchange_code_for_token(code)
|
|
if not result or not result.get("user"):
|
|
# 重定向到前端登录页,带错误参数
|
|
return RedirectResponse(url=f"{APP_FRONTEND_URL}/#/login?error=casdoor_failed")
|
|
|
|
casdoor_user = result["user"]
|
|
user = await get_or_create_user(db, casdoor_user)
|
|
|
|
# 生成平台 JWT token
|
|
token_data = {"sub": user.id, "username": user.username, "role": user.role}
|
|
access_token = create_access_token(token_data)
|
|
|
|
# 重定向到前端,带 token
|
|
redirect_url = f"{APP_FRONTEND_URL}/#/login?token={access_token}&sso=casdoor"
|
|
return RedirectResponse(url=redirect_url)
|
|
|
|
|
|
@router.get("/casdoor/status")
|
|
async def casdoor_status():
|
|
"""检查 Casdoor SSO 是否可用"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5) as client:
|
|
resp = await client.get(f"{CASDOOR_ENDPOINT}/api/health")
|
|
if resp.status_code == 200:
|
|
return {"enabled": True, "endpoint": CASDOOR_ENDPOINT}
|
|
except Exception:
|
|
pass
|
|
return {"enabled": False, "endpoint": CASDOOR_ENDPOINT}
|
|
|
|
|
|
# 配置说明:
|
|
# 1. 在 Casdoor 中创建应用,获取 Client ID 和 Client Secret
|
|
# 2. 设置环境变量:
|
|
# - CASDOOR_ENDPOINT=http://casdoor:8000
|
|
# - CASDOOR_CLIENT_ID=your_client_id
|
|
# - CASDOOR_CLIENT_SECRET=your_client_secret
|
|
# - CASDOOR_REDIRECT_URI=http://your-domain/app/api/auth/casdoor/callback
|
|
# 3. 在 User 模型中添加 casdoor_id 字段 (可选):
|
|
# casdoor_id = Column(String(128), unique=True, nullable=True)
|