|
@@ -4,7 +4,8 @@ import uuid
|
|
|
import os
|
|
import os
|
|
|
|
|
|
|
|
import httpx
|
|
import httpx
|
|
|
-from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
|
|
+from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
|
+from fastapi.responses import HTMLResponse
|
|
|
from pydantic import BaseModel
|
|
from pydantic import BaseModel
|
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
@@ -37,6 +38,10 @@ def _get_sso_config():
|
|
|
sso = data.get("sso", {})
|
|
sso = data.get("sso", {})
|
|
|
except Exception:
|
|
except Exception:
|
|
|
sso = {}
|
|
sso = {}
|
|
|
|
|
+ # sso_enable_redirect 控制是否启用SSO重定向
|
|
|
|
|
+ enabled = sso.get("sso_enable_redirect")
|
|
|
|
|
+ if enabled is None:
|
|
|
|
|
+ enabled = True # 默认启用SSO
|
|
|
return {
|
|
return {
|
|
|
"base_url": (sso.get("sso_base_url") or SSO_BASE_URL).rstrip("/"),
|
|
"base_url": (sso.get("sso_base_url") or SSO_BASE_URL).rstrip("/"),
|
|
|
"client_id": sso.get("sso_client_id") or SSO_CLIENT_ID,
|
|
"client_id": sso.get("sso_client_id") or SSO_CLIENT_ID,
|
|
@@ -45,7 +50,7 @@ def _get_sso_config():
|
|
|
"admin_redirect_uri": sso.get("sso_admin_redirect_uri") or SSO_ADMIN_REDIRECT_URI,
|
|
"admin_redirect_uri": sso.get("sso_admin_redirect_uri") or SSO_ADMIN_REDIRECT_URI,
|
|
|
"scope": sso.get("sso_scope") or SSO_SCOPE,
|
|
"scope": sso.get("sso_scope") or SSO_SCOPE,
|
|
|
"logout_url": sso.get("sso_logout_redirect_url") or SSO_LOGOUT_URL,
|
|
"logout_url": sso.get("sso_logout_redirect_url") or SSO_LOGOUT_URL,
|
|
|
- "enabled": bool(sso.get("sso_enabled", True)),
|
|
|
|
|
|
|
+ "enabled": bool(enabled),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -60,6 +65,7 @@ def get_sso_public_config():
|
|
|
f"{cfg['base_url']}/oauth/authorize"
|
|
f"{cfg['base_url']}/oauth/authorize"
|
|
|
f"?response_type=code&client_id={cfg['client_id']}"
|
|
f"?response_type=code&client_id={cfg['client_id']}"
|
|
|
f"&redirect_uri={cfg['redirect_uri']}&scope={cfg['scope']}"
|
|
f"&redirect_uri={cfg['redirect_uri']}&scope={cfg['scope']}"
|
|
|
|
|
+ f"&state=/"
|
|
|
)
|
|
)
|
|
|
return {
|
|
return {
|
|
|
"sso_enabled": cfg["enabled"] and bool(cfg["client_id"]),
|
|
"sso_enabled": cfg["enabled"] and bool(cfg["client_id"]),
|
|
@@ -156,6 +162,122 @@ async def exchange_code(req: CodeRequest, db: Session = Depends(get_db)):
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ============ OAuth 回调(处理 SSO 服务器的 code 回传) ============
|
|
|
|
|
+
|
|
|
|
|
+async def _exchange_code_and_build_html(code: str, redirect_path: str, is_admin: bool = False) -> str:
|
|
|
|
|
+ """用 code 换 local JWT,返回自动登录并跳转的 HTML 页面。"""
|
|
|
|
|
+ from app.database import SessionLocal
|
|
|
|
|
+ from app.models.user import User
|
|
|
|
|
+ from app.models.admin import AdminUser
|
|
|
|
|
+ from app.services.user_service import UserService
|
|
|
|
|
+ from app.services.auth_service import AuthService
|
|
|
|
|
+ from app.services.admin_auth_service import AdminAuthService
|
|
|
|
|
+ from app.schemas.user_schema import UserCreate
|
|
|
|
|
+
|
|
|
|
|
+ cfg = _get_sso_config()
|
|
|
|
|
+
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=15) as client:
|
|
|
|
|
+ try:
|
|
|
|
|
+ resp = await client.post(
|
|
|
|
|
+ f"{cfg['base_url']}/oauth/token",
|
|
|
|
|
+ data={
|
|
|
|
|
+ "grant_type": "authorization_code",
|
|
|
|
|
+ "code": code,
|
|
|
|
|
+ "redirect_uri": cfg["admin_redirect_uri"] if is_admin else cfg["redirect_uri"],
|
|
|
|
|
+ "client_id": cfg["client_id"],
|
|
|
|
|
+ "client_secret": cfg["client_secret"],
|
|
|
|
|
+ },
|
|
|
|
|
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
|
|
|
+ )
|
|
|
|
|
+ except httpx.RequestError:
|
|
|
|
|
+ return "<html><body><script>alert('Cannot connect to SSO');window.location='/login';</script></body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ if resp.status_code != 200:
|
|
|
|
|
+ try:
|
|
|
|
|
+ err = resp.json()
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ err = {}
|
|
|
|
|
+ msg = err.get("error_description") or "SSO login failed"
|
|
|
|
|
+ import html as _html
|
|
|
|
|
+ return f"<html><body><script>alert('{_html.escape(msg)}');window.location='/login';</script></body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ access_token = resp.json().get("access_token")
|
|
|
|
|
+ if not access_token:
|
|
|
|
|
+ return "<html><body><script>alert('No access_token from SSO');window.location='/login';</script></body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ info_resp = await client.get(
|
|
|
|
|
+ f"{cfg['base_url']}/oauth/userinfo",
|
|
|
|
|
+ headers={"Authorization": f"Bearer {access_token}"},
|
|
|
|
|
+ )
|
|
|
|
|
+ except httpx.RequestError:
|
|
|
|
|
+ return "<html><body><script>alert('Failed to get userinfo');window.location='/login';</script></body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ if info_resp.status_code != 200:
|
|
|
|
|
+ return "<html><body><script>alert('Failed to get userinfo');window.location='/login';</script></body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ userinfo = info_resp.json()
|
|
|
|
|
+
|
|
|
|
|
+ username = userinfo.get("username") or userinfo.get("sub") or ""
|
|
|
|
|
+ if not username:
|
|
|
|
|
+ return "<html><body><script>alert('No username from SSO');window.location='/login';</script></body></html>"
|
|
|
|
|
+
|
|
|
|
|
+ db = SessionLocal()
|
|
|
|
|
+ try:
|
|
|
|
|
+ if is_admin:
|
|
|
|
|
+ admin = db.query(AdminUser).filter(AdminUser.username == username).first()
|
|
|
|
|
+ if not admin:
|
|
|
|
|
+ admin = AdminUser(
|
|
|
|
|
+ username=username,
|
|
|
|
|
+ password_hash=AdminAuthService.hash_password(uuid.uuid4().hex[:16]),
|
|
|
|
|
+ nickname=userinfo.get("real_name") or username,
|
|
|
|
|
+ status="active",
|
|
|
|
|
+ )
|
|
|
|
|
+ db.add(admin)
|
|
|
|
|
+ db.commit()
|
|
|
|
|
+ db.refresh(admin)
|
|
|
|
|
+ local_token = AdminAuthService.create_token(admin.id, admin.username)
|
|
|
|
|
+ else:
|
|
|
|
|
+ user = db.query(User).filter(User.username == username).first()
|
|
|
|
|
+ if not user:
|
|
|
|
|
+ payload = {"username": username, "password": uuid.uuid4().hex[:16], "nickname": userinfo.get("real_name") or username}
|
|
|
|
|
+ if userinfo.get("email"):
|
|
|
|
|
+ payload["email"] = userinfo["email"]
|
|
|
|
|
+ user = UserService(db).create_user(UserCreate(**payload))
|
|
|
|
|
+ elif userinfo.get("email") and user.email != userinfo["email"]:
|
|
|
|
|
+ user.email = userinfo["email"]
|
|
|
|
|
+ db.commit()
|
|
|
|
|
+ local_token = AuthService.create_access_token(user.id)
|
|
|
|
|
+ finally:
|
|
|
|
|
+ db.close()
|
|
|
|
|
+
|
|
|
|
|
+ import html as _html
|
|
|
|
|
+ safe_redirect = _html.escape(redirect_path or "/")
|
|
|
|
|
+ return (
|
|
|
|
|
+ "<!DOCTYPE html><html><head><title>登录中...</title></head><body>"
|
|
|
|
|
+ "<script>"
|
|
|
|
|
+ f"localStorage.setItem('aigc_space_token','{local_token}');"
|
|
|
|
|
+ f"window.location.replace('{safe_redirect}');"
|
|
|
|
|
+ "</script>"
|
|
|
|
|
+ "<p>正在登录,请稍候...</p>"
|
|
|
|
|
+ "</body></html>"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.get("/api/oauth/callback")
|
|
|
|
|
+async def oauth_user_callback(code: str = Query(...), state: str = Query(default="/")):
|
|
|
|
|
+ """用户端 OAuth 回调:SSO 服务器 redirect 到此地址,后端换 token 后返回自动登录 HTML。"""
|
|
|
|
|
+ html_content = await _exchange_code_and_build_html(code, state, is_admin=False)
|
|
|
|
|
+ return HTMLResponse(content=html_content)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@router.get("/api/admin/oauth/callback")
|
|
|
|
|
+async def oauth_admin_callback(code: str = Query(...), state: str = Query(default="/admin/")):
|
|
|
|
|
+ """管理后台 OAuth 回调:同上,但签发管理员 token。"""
|
|
|
|
|
+ html_content = await _exchange_code_and_build_html(code, state, is_admin=True)
|
|
|
|
|
+ return HTMLResponse(content=html_content)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# ============ 管理员 SSO 接口 ============
|
|
# ============ 管理员 SSO 接口 ============
|
|
|
|
|
|
|
|
@router.get("/api/admin/sso/config")
|
|
@router.get("/api/admin/sso/config")
|
|
@@ -166,6 +288,7 @@ def get_admin_sso_config():
|
|
|
f"{cfg['base_url']}/oauth/authorize"
|
|
f"{cfg['base_url']}/oauth/authorize"
|
|
|
f"?response_type=code&client_id={cfg['client_id']}"
|
|
f"?response_type=code&client_id={cfg['client_id']}"
|
|
|
f"&redirect_uri={cfg['admin_redirect_uri']}&scope={cfg['scope']}"
|
|
f"&redirect_uri={cfg['admin_redirect_uri']}&scope={cfg['scope']}"
|
|
|
|
|
+ f"&state=/admin/"
|
|
|
)
|
|
)
|
|
|
return {
|
|
return {
|
|
|
"sso_enabled": cfg["enabled"] and bool(cfg["client_id"]),
|
|
"sso_enabled": cfg["enabled"] and bool(cfg["client_id"]),
|