oauth.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """
  2. OAuth 2.0 认证路由
  3. 处理 SSO 登录流程、token 刷新和用户信息查询。
  4. 所有认证统一走 SSO,不再本地签发 JWT。
  5. """
  6. import logging
  7. from fastapi import APIRouter, HTTPException, Query, Request, status
  8. from pydantic import BaseModel
  9. from typing import Optional
  10. from config import settings
  11. from services.oauth_service import OAuthService
  12. from services.auth_service import AuthService
  13. from middleware.auth_middleware import token_cache
  14. logger = logging.getLogger(__name__)
  15. router = APIRouter(prefix="/api/oauth", tags=["oauth"])
  16. class OAuthLoginResponse(BaseModel):
  17. """OAuth 登录响应"""
  18. authorization_url: str
  19. state: str
  20. class SSOTokenResponse(BaseModel):
  21. """SSO Token 响应(透传 SSO 中心的 token)"""
  22. access_token: str
  23. refresh_token: str
  24. token_type: str = "bearer"
  25. user: dict
  26. class RefreshRequest(BaseModel):
  27. """Token 刷新请求"""
  28. refresh_token: str
  29. class UserResponse(BaseModel):
  30. """用户信息响应"""
  31. id: str
  32. username: str
  33. email: str
  34. role: str
  35. created_at: str
  36. @router.get("/login", response_model=OAuthLoginResponse)
  37. async def oauth_login():
  38. """
  39. 启动 OAuth 登录流程。
  40. 生成授权 URL 和 state 参数,前端需要保存 state 并重定向到授权 URL。
  41. """
  42. if not settings.OAUTH_ENABLED:
  43. raise HTTPException(status_code=503, detail="SSO 认证未配置")
  44. state = OAuthService.generate_state()
  45. authorization_url = OAuthService.get_authorization_url(state)
  46. return OAuthLoginResponse(
  47. authorization_url=authorization_url,
  48. state=state
  49. )
  50. @router.get("/callback")
  51. async def oauth_callback(
  52. code: str = Query(..., description="OAuth 授权码"),
  53. state: str = Query(..., description="State 参数"),
  54. ):
  55. """
  56. OAuth 回调端点。
  57. 用授权码换取 SSO token,获取用户信息并同步到本地数据库,
  58. 直接返回 SSO 的 access_token 和 refresh_token(不再本地签发 JWT)。
  59. """
  60. logger.info(f"OAuth callback received: code={code[:10]}..., state={state[:10]}...")
  61. if not settings.OAUTH_ENABLED:
  62. raise HTTPException(status_code=503, detail="SSO 认证未配置")
  63. try:
  64. # 1. 用授权码换取 SSO token
  65. logger.debug("Exchanging code for token...")
  66. token_data = await OAuthService.exchange_code_for_token(code)
  67. access_token = token_data.get("access_token")
  68. refresh_token = token_data.get("refresh_token", "")
  69. if not access_token:
  70. raise HTTPException(status_code=400, detail="未能获取访问令牌")
  71. # 2. 使用 SSO token 获取完整用户信息(含角色)
  72. logger.debug("Verifying SSO token and getting user info...")
  73. try:
  74. user_info = await OAuthService.verify_sso_token(access_token)
  75. logger.debug(f"User info: {user_info.get('username')}, role: {user_info.get('role')}")
  76. except HTTPException as e:
  77. logger.error(f"verify_sso_token failed: status={e.status_code}, detail={e.detail}")
  78. raise
  79. except Exception as e:
  80. logger.error(f"verify_sso_token unexpected error: {e}", exc_info=True)
  81. raise
  82. # 3. 同步用户到本地数据库(含角色映射)
  83. logger.debug("Syncing user to local database...")
  84. try:
  85. user = OAuthService.sync_user_from_oauth(user_info)
  86. logger.debug(f"User synced: id={user.id}, username={user.username}, role={user.role}")
  87. except Exception as e:
  88. logger.error(f"sync_user_from_oauth failed: {e}", exc_info=True)
  89. raise
  90. # 4. 缓存 token → 用户信息映射
  91. token_cache.set(access_token, user_info)
  92. # 5. 直接返回 SSO token(不再本地签发 JWT)
  93. logger.info(f"OAuth login successful for user: {user.username}")
  94. return SSOTokenResponse(
  95. access_token=access_token,
  96. refresh_token=refresh_token,
  97. token_type="bearer",
  98. user={
  99. "id": user.id,
  100. "username": user.username,
  101. "email": user.email,
  102. "role": user.role,
  103. "created_at": str(user.created_at)
  104. }
  105. )
  106. except HTTPException:
  107. raise
  108. except Exception as e:
  109. logger.error(f"OAuth callback error: {e}", exc_info=True)
  110. raise HTTPException(
  111. status_code=400,
  112. detail=f"OAuth 登录失败: {str(e)}"
  113. )
  114. @router.post("/refresh")
  115. async def oauth_refresh(request_body: RefreshRequest):
  116. """
  117. Token 刷新端点。
  118. 将 refresh 请求转发到 SSO 中心,返回新的 token。
  119. """
  120. logger.info(f"Token refresh requested, refresh_token={request_body.refresh_token[:20]}...")
  121. if not settings.OAUTH_ENABLED:
  122. raise HTTPException(status_code=503, detail="SSO 认证未配置")
  123. try:
  124. token_data = await OAuthService.refresh_sso_token(request_body.refresh_token)
  125. logger.info("Token refresh successful")
  126. return {
  127. "access_token": token_data.get("access_token"),
  128. "refresh_token": token_data.get("refresh_token", ""),
  129. "token_type": token_data.get("token_type", "bearer"),
  130. }
  131. except HTTPException as e:
  132. logger.error(f"Token refresh failed: status={e.status_code}, detail={e.detail}")
  133. raise
  134. except Exception as e:
  135. logger.error(f"Token refresh unexpected error: {e}", exc_info=True)
  136. raise HTTPException(
  137. status_code=400,
  138. detail=f"Token 刷新失败: {str(e)}"
  139. )
  140. @router.get("/me", response_model=UserResponse)
  141. async def get_current_user(request: Request):
  142. """
  143. 获取当前认证用户信息。
  144. 用户信息由 AuthMiddleware 从 SSO 验证后填充到 request.state。
  145. """
  146. user_data = getattr(request.state, "user", None)
  147. if not user_data:
  148. raise HTTPException(
  149. status_code=status.HTTP_401_UNAUTHORIZED,
  150. detail="未认证"
  151. )
  152. # 从本地数据库获取完整用户信息
  153. user = AuthService.get_current_user(user_data["id"])
  154. return UserResponse(
  155. id=user.id,
  156. username=user.username,
  157. email=user.email,
  158. role=user.role,
  159. created_at=str(user.created_at)
  160. )
  161. @router.get("/status")
  162. async def oauth_status():
  163. """获取 OAuth 配置状态"""
  164. return {
  165. "enabled": settings.OAUTH_ENABLED,
  166. "provider": "SSO" if settings.OAUTH_ENABLED else None,
  167. "base_url": settings.OAUTH_BASE_URL if settings.OAUTH_ENABLED else None
  168. }