oauth.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """
  2. OAuth 2.0 认证路由
  3. 处理 SSO 登录流程、本地 JWT 签发、token 刷新和用户信息查询。
  4. """
  5. import logging
  6. import httpx
  7. from fastapi import APIRouter, HTTPException, Request, status
  8. from pydantic import BaseModel
  9. from config import settings
  10. from services.oauth_service import OAuthService
  11. from services.auth_service import AuthService
  12. from services import jwt_service
  13. logger = logging.getLogger(__name__)
  14. router = APIRouter(prefix="/api/oauth", tags=["oauth"])
  15. class OAuthLoginResponse(BaseModel):
  16. """OAuth 登录响应"""
  17. authorization_url: str
  18. state: str
  19. class SSOTokenResponse(BaseModel):
  20. """SSO Token 响应(本地签发的 JWT)"""
  21. token: str
  22. refresh_token: str
  23. token_type: str = "bearer"
  24. user: dict
  25. class ExchangeCodeRequest(BaseModel):
  26. """授权码交换请求"""
  27. code: str
  28. class ExchangeCodeResponse(BaseModel):
  29. """授权码交换响应(本地签发的 JWT)"""
  30. token: str
  31. refresh_token: str
  32. token_type: str = "bearer"
  33. user: dict
  34. class RefreshRequest(BaseModel):
  35. """Token 刷新请求"""
  36. refresh_token: str
  37. class UserResponse(BaseModel):
  38. """用户信息响应"""
  39. id: str
  40. username: str
  41. email: str
  42. role: str
  43. created_at: str
  44. @router.get("/login", response_model=OAuthLoginResponse)
  45. async def oauth_login():
  46. """
  47. 启动 OAuth 登录流程。
  48. 生成授权 URL 和 state 参数。
  49. """
  50. state = OAuthService.generate_state()
  51. authorization_url = OAuthService.get_authorization_url(state)
  52. logger.info(f"[OAuth] /api/oauth/login 被调用")
  53. logger.info(f"[OAuth] state: {state}")
  54. logger.info(f"[OAuth] authorization_url: {authorization_url}")
  55. logger.info(f"[OAuth] SSO_BASE_URL: {settings.SSO_BASE_URL}")
  56. logger.info(f"[OAuth] SSO_REDIRECT_URI: {settings.SSO_REDIRECT_URI}")
  57. logger.info(f"[OAuth] SSO_CLIENT_ID: {settings.SSO_CLIENT_ID[:10]}...")
  58. return OAuthLoginResponse(
  59. authorization_url=authorization_url,
  60. state=state,
  61. )
  62. @router.post("/exchange-code", response_model=ExchangeCodeResponse)
  63. async def exchange_code(request_body: ExchangeCodeRequest):
  64. """
  65. 授权码交换端点(前端调用)。
  66. 前端从 SSO 回调拿到 code 后,调用此接口换取本地 JWT。
  67. """
  68. logger.info("=" * 60)
  69. logger.info("[OAuth] /api/oauth/exchange-code 请求到达")
  70. logger.info(f"[OAuth] 请求体: code={request_body.code[:15] if request_body.code else '(空)'}...")
  71. logger.info(f"[OAuth] code 长度: {len(request_body.code) if request_body.code else 0}")
  72. logger.info(f"[OAuth] code 完整值: {request_body.code}")
  73. if not request_body.code:
  74. logger.error("[OAuth] 授权码为空!前端未传递 code 参数")
  75. raise HTTPException(status_code=400, detail="授权码为空,请检查前端是否正确传递 code 参数")
  76. try:
  77. # 1. 用授权码换取 SSO access_token
  78. logger.info(f"[OAuth] 开始向 SSO 换取 token, SSO地址: {settings.SSO_BASE_URL}{settings.SSO_TOKEN_ENDPOINT}")
  79. token_data = await OAuthService.exchange_code_for_token(request_body.code)
  80. sso_access_token = token_data.get("access_token")
  81. if not sso_access_token:
  82. logger.error(f"[OAuth] SSO 返回的 token 响应中没有 access_token, 完整响应: {token_data}")
  83. raise HTTPException(status_code=400, detail="未能获取访问令牌")
  84. logger.info(f"[OAuth] SSO token 获取成功, access_token 前20字符: {sso_access_token[:20]}...")
  85. # 2. 获取完整用户信息(含角色)
  86. logger.info("[OAuth] 开始获取用户信息")
  87. user_info = await OAuthService.get_user_profile(sso_access_token)
  88. logger.info(f"[OAuth] 用户信息: {user_info}")
  89. # 3. 同步用户到本地数据库
  90. logger.info(f"[OAuth] 开始同步用户到本地数据库, username={user_info.get('username')}")
  91. user = OAuthService.sync_user_from_oauth(user_info)
  92. logger.info(f"[OAuth] 用户同步完成, user.id={user.id}, user.role={user.role}")
  93. except ValueError as e:
  94. logger.error(f"[OAuth] 用户角色映射失败: {e}")
  95. raise HTTPException(status_code=403, detail=str(e))
  96. except HTTPException:
  97. raise
  98. except Exception as e:
  99. logger.error(f"[OAuth] 授权码交换异常: {e}", exc_info=True)
  100. raise HTTPException(status_code=500, detail=f"登录失败: {str(e)}")
  101. # 4. 签发本地 JWT
  102. access_token = jwt_service.create_access_token(
  103. user_id=user.id,
  104. username=user.username,
  105. email=user.email,
  106. role=user.role,
  107. )
  108. refresh_token = jwt_service.create_refresh_token(user_id=user.id)
  109. logger.info(f"Code exchange successful for user: {user.username}")
  110. return ExchangeCodeResponse(
  111. token=access_token,
  112. refresh_token=refresh_token,
  113. token_type="bearer",
  114. user={
  115. "id": user.id,
  116. "username": user.username,
  117. "email": user.email,
  118. "role": user.role,
  119. "created_at": str(user.created_at),
  120. },
  121. )
  122. @router.post("/refresh")
  123. async def oauth_refresh(request_body: RefreshRequest):
  124. """
  125. Token 刷新端点。
  126. 验证 refresh_token(本地 JWT),签发新的 access_token + refresh_token。
  127. """
  128. logger.info("Token refresh requested")
  129. try:
  130. payload = jwt_service.verify_token(request_body.refresh_token)
  131. if payload.get("type") != "refresh":
  132. raise HTTPException(
  133. status_code=status.HTTP_401_UNAUTHORIZED,
  134. detail="无效的 refresh token",
  135. )
  136. user_id = payload.get("sub")
  137. # 从数据库获取用户信息
  138. user = AuthService.get_current_user(user_id)
  139. if not user:
  140. raise HTTPException(
  141. status_code=status.HTTP_401_UNAUTHORIZED,
  142. detail="用户不存在",
  143. )
  144. new_access_token = jwt_service.create_access_token(
  145. user_id=user.id,
  146. username=user.username,
  147. email=user.email,
  148. role=user.role,
  149. )
  150. new_refresh_token = jwt_service.create_refresh_token(user_id=user.id)
  151. return {
  152. "token": new_access_token,
  153. "refresh_token": new_refresh_token,
  154. "token_type": "bearer",
  155. }
  156. except HTTPException:
  157. raise
  158. except Exception as e:
  159. logger.error(f"Token refresh unexpected error: {e}", exc_info=True)
  160. raise HTTPException(
  161. status_code=400,
  162. detail=f"Token 刷新失败: {str(e)}",
  163. )
  164. @router.post("/logout")
  165. async def oauth_logout(request: Request):
  166. """
  167. 登出端点。
  168. 可选通知 SSO 注销,返回统一认证平台登录页面 URL 供前端跳转。
  169. """
  170. auth_header = request.headers.get("Authorization", "")
  171. token = auth_header.replace("Bearer ", "") if auth_header.startswith("Bearer ") else None
  172. if token and settings.SSO_REVOKE_ENDPOINT:
  173. # 通知 SSO 注销
  174. try:
  175. async with httpx.AsyncClient(timeout=5.0) as client:
  176. await client.post(
  177. f"{settings.SSO_BASE_URL}{settings.SSO_REVOKE_ENDPOINT}",
  178. headers={"Authorization": f"Bearer {token}"},
  179. )
  180. except Exception as e:
  181. logger.warning(f"SSO revoke failed (non-critical): {e}")
  182. return {
  183. "message": "登出成功",
  184. "logout_url": settings.SSO_LOGOUT_REDIRECT_URL,
  185. }
  186. @router.get("/me", response_model=UserResponse)
  187. async def get_current_user(request: Request):
  188. """
  189. 获取当前认证用户信息。
  190. 用户信息由 AuthMiddleware 从 JWT 验证后填充到 request.state。
  191. """
  192. user_data = getattr(request.state, "user", None)
  193. if not user_data:
  194. raise HTTPException(
  195. status_code=status.HTTP_401_UNAUTHORIZED,
  196. detail="未认证",
  197. )
  198. user = AuthService.get_current_user(user_data["id"])
  199. return UserResponse(
  200. id=user.id,
  201. username=user.username,
  202. email=user.email,
  203. role=user.role,
  204. created_at=str(user.created_at),
  205. )
  206. @router.get("/status")
  207. async def oauth_status():
  208. """获取 SSO 配置状态"""
  209. return {
  210. "enabled": settings.SSO_ENABLED,
  211. "provider": "SSO" if settings.SSO_ENABLED else None,
  212. "base_url": settings.SSO_BASE_URL if settings.SSO_ENABLED else None,
  213. }