173 lines
6.8 KiB
Python
173 lines
6.8 KiB
Python
"""
|
|
카카오 OAuth API 클라이언트
|
|
|
|
카카오 로그인 인증 흐름을 처리하는 클라이언트입니다.
|
|
"""
|
|
|
|
import logging
|
|
|
|
import aiohttp
|
|
from fastapi import HTTPException, status
|
|
|
|
from config import kakao_settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from app.user.schemas.user_schema import KakaoTokenResponse, KakaoUserInfo
|
|
|
|
|
|
# =============================================================================
|
|
# 카카오 OAuth 예외 클래스 정의
|
|
# =============================================================================
|
|
class KakaoException(HTTPException):
|
|
"""카카오 관련 기본 예외"""
|
|
|
|
def __init__(self, status_code: int, code: str, message: str):
|
|
super().__init__(status_code=status_code, detail={"code": code, "message": message})
|
|
|
|
|
|
class KakaoAuthFailedError(KakaoException):
|
|
"""카카오 인증 실패"""
|
|
|
|
def __init__(self, message: str = "카카오 인증에 실패했습니다."):
|
|
super().__init__(status.HTTP_400_BAD_REQUEST, "KAKAO_AUTH_FAILED", message)
|
|
|
|
|
|
class KakaoAPIError(KakaoException):
|
|
"""카카오 API 호출 오류"""
|
|
|
|
def __init__(self, message: str = "카카오 API 호출 중 오류가 발생했습니다."):
|
|
super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, "KAKAO_API_ERROR", message)
|
|
|
|
|
|
class KakaoOAuthClient:
|
|
"""
|
|
카카오 OAuth API 클라이언트
|
|
|
|
카카오 로그인 인증 흐름:
|
|
1. get_authorization_url()로 카카오 로그인 페이지 URL 획득
|
|
2. 사용자가 카카오에서 로그인 후 인가 코드(code) 발급
|
|
3. get_access_token()으로 인가 코드를 액세스 토큰으로 교환
|
|
4. get_user_info()로 사용자 정보 조회
|
|
"""
|
|
|
|
AUTH_URL = "https://kauth.kakao.com/oauth/authorize"
|
|
TOKEN_URL = "https://kauth.kakao.com/oauth/token"
|
|
USER_INFO_URL = "https://kapi.kakao.com/v2/user/me"
|
|
|
|
def __init__(self) -> None:
|
|
self.client_id = kakao_settings.KAKAO_CLIENT_ID
|
|
self.client_secret = kakao_settings.KAKAO_CLIENT_SECRET
|
|
self.redirect_uri = kakao_settings.KAKAO_REDIRECT_URI
|
|
|
|
def get_authorization_url(self) -> str:
|
|
"""
|
|
카카오 로그인 페이지 URL 반환
|
|
|
|
Returns:
|
|
카카오 OAuth 인증 페이지 URL
|
|
"""
|
|
auth_url = (
|
|
f"{self.AUTH_URL}"
|
|
f"?client_id={self.client_id}"
|
|
f"&redirect_uri={self.redirect_uri}"
|
|
f"&response_type=code"
|
|
)
|
|
logger.info(f"[KAKAO] 인증 URL 생성 - redirect_uri: {self.redirect_uri}")
|
|
logger.debug(f"[KAKAO] 인증 URL 상세 - auth_url: {auth_url}")
|
|
return auth_url
|
|
|
|
async def get_access_token(self, code: str) -> KakaoTokenResponse:
|
|
"""
|
|
인가 코드로 액세스 토큰 획득
|
|
|
|
Args:
|
|
code: 카카오 로그인 후 발급받은 인가 코드
|
|
|
|
Returns:
|
|
KakaoTokenResponse: 카카오 토큰 정보
|
|
|
|
Raises:
|
|
KakaoAuthFailedError: 토큰 발급 실패 시
|
|
KakaoAPIError: API 호출 오류 시
|
|
"""
|
|
logger.info(f"[KAKAO] 액세스 토큰 요청 시작 - code: {code[:20]}...")
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
data = {
|
|
"grant_type": "authorization_code",
|
|
"client_id": self.client_id,
|
|
"redirect_uri": self.redirect_uri,
|
|
"code": code,
|
|
}
|
|
|
|
if self.client_secret:
|
|
data["client_secret"] = self.client_secret
|
|
|
|
logger.debug(f"[KAKAO] 토큰 요청 데이터 - redirect_uri: {self.redirect_uri}, client_id: {self.client_id[:10]}...")
|
|
|
|
async with session.post(self.TOKEN_URL, data=data) as response:
|
|
result = await response.json()
|
|
logger.debug(f"[KAKAO] 토큰 응답 상태 - status: {response.status}")
|
|
|
|
if "error" in result:
|
|
error_desc = result.get(
|
|
"error_description", result.get("error", "알 수 없는 오류")
|
|
)
|
|
logger.error(f"[KAKAO] 토큰 발급 실패 - error: {result.get('error')}, description: {error_desc}")
|
|
raise KakaoAuthFailedError(f"카카오 토큰 발급 실패: {error_desc}")
|
|
|
|
logger.info("[KAKAO] 액세스 토큰 발급 성공")
|
|
logger.debug(f"[KAKAO] 토큰 정보 - token_type: {result.get('token_type')}, expires_in: {result.get('expires_in')}")
|
|
return KakaoTokenResponse(**result)
|
|
|
|
except KakaoAuthFailedError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}")
|
|
raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}")
|
|
|
|
async def get_user_info(self, access_token: str) -> KakaoUserInfo:
|
|
"""
|
|
액세스 토큰으로 사용자 정보 조회
|
|
|
|
Args:
|
|
access_token: 카카오 액세스 토큰
|
|
|
|
Returns:
|
|
KakaoUserInfo: 카카오 사용자 정보
|
|
|
|
Raises:
|
|
KakaoAuthFailedError: 사용자 정보 조회 실패 시
|
|
KakaoAPIError: API 호출 오류 시
|
|
"""
|
|
logger.info("[KAKAO] 사용자 정보 조회 시작")
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
|
|
async with session.get(self.USER_INFO_URL, headers=headers) as response:
|
|
result = await response.json()
|
|
logger.debug(f"[KAKAO] 사용자 정보 응답 상태 - status: {response.status}")
|
|
|
|
if "id" not in result:
|
|
logger.error(f"[KAKAO] 사용자 정보 조회 실패 - response: {result}")
|
|
raise KakaoAuthFailedError("카카오 사용자 정보를 가져올 수 없습니다.")
|
|
|
|
kakao_id = result.get("id")
|
|
kakao_account = result.get("kakao_account", {})
|
|
profile = kakao_account.get("profile", {})
|
|
|
|
logger.info(f"[KAKAO] 사용자 정보 조회 성공 - kakao_id: {kakao_id}")
|
|
logger.debug(f"[KAKAO] 사용자 상세 정보 - nickname: {profile.get('nickname')}, email: {kakao_account.get('email')}")
|
|
return KakaoUserInfo(**result)
|
|
|
|
except KakaoAuthFailedError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}")
|
|
raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}")
|
|
|
|
|
|
kakao_client = KakaoOAuthClient()
|