이전 내용
[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(구글)
이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 8: MVC 패턴이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 7: 보완(회원가입 등)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 6: 프론트엔드 페이지 개선
puppy-foot-it.tistory.com
FastAPI에서 OAuth2 구현
2. 카카오
구글 로그인을 구현했으니, 이번에는 카카오 로그인 기능도 구현해 본다.
카카오 로그인 기능을 추가하는 상세한 내용은 하단 링크를 확인하면 된다. (카카오 디벨로퍼에서 ID와 SecretKey 발급 받고, Redirect URI 추가 등은 프레임워크와 무관하게 동일)
카카오 디벨로퍼: https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite
[Java] Spring Boot: 카카오 로그인 기능 추가하기
이전 내용 [Java] Spring Boot: 구글 로그인 기능 추가하기이전 내용 전반적인 순서1. Gradle 또는 Maven 의존성 추가2. 구글 API Console 설정 - 구글 개발자 콘솔 프로젝트 생성 → OAuth 2.0 클라이언트 ID 생성
puppy-foot-it.tistory.com
카카오 디벨로퍼에서 REST_API_KEY, SECRET_KEY를 발급받고, REDIRECT_URI를 등록(http://, https:// 두 개 등록)한 후, .env 파일에 해당 정보를 등록한다.
그리고 oauth 디렉터리 내에 kakao.py 파일을 만들어서 코드를 작성한다.
◆ kakao.py (oauth 디렉터리 내)
scope(범위)의 경우, 카카오 디벨로퍼에 나온 항목과 동일하게 적어준다.
@router.get("/auth/kakao/login")
def login():
auth_url = (
f"{KAKAO_AUTH_URL}?response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=profile_nickname, account_email" # 동의항목: 카카오 디벨로퍼와 동일하게
)
return RedirectResponse(url=auth_url)
[kakao.py 전체 코드]
import os
import httpx
from fastapi import APIRouter, Request, Depends
from dotenv import load_dotenv
from fastapi.templating import Jinja2Templates
from dependencies import get_db
from sqlalchemy.orm import Session
from controllers import create_or_update_user
from fastapi.responses import RedirectResponse
load_dotenv()
router = APIRouter()
templates = Jinja2Templates(directory="templates")
KAKAO_AUTH_URL = "https://kauth.kakao.com/oauth/authorize"
KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token"
KAKAO_USERINFO_URL = "https://kapi.kakao.com/v2/user/me"
CLIENT_ID = os.getenv("KAKAO_REST_API_KEY")
CLIENT_SECRET = os.getenv("KAKAO_CLIENT_SECRET")
REDIRECT_URI = os.getenv("KAKAO_REDIRECT_URI")
@router.get("/auth/kakao/login")
def login():
auth_url = (
f"{KAKAO_AUTH_URL}?response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=profile_nickname, account_email" # 동의항목: 카카오 디벨로퍼와 동일하게
)
return RedirectResponse(url=auth_url)
@router.get("/kakao/login/callback")
async def callback(request: Request, db: Session = Depends(get_db)): # get_db() 사용
code = request.query_params.get("code")
async with httpx.AsyncClient() as client:
token_res = await client.post(KAKAO_TOKEN_URL, data={
'code': code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'
})
if token_res.status_code != 200:
return {"error": "Failed to retrieve token"}
access_token = token_res.json().get("access_token")
# 사용자 정보 요청
userinfo_res = await client.get(KAKAO_USERINFO_URL, headers={
'Authorization': f'Bearer {access_token}'
})
if userinfo_res.status_code != 200:
return {"error": "Failed to retrieve user information"}
# 사용자 정보 처리
user_info = userinfo_res.json()
kakao_account = user_info.get("kakao_account", {})
profile = kakao_account.get("profile", {})
username = profile.get("nickname", "User")
email = kakao_account.get("email")
kakao_id = user_info.get("id")
user = create_or_update_user(db, { # db를 사용하여 사용자 정보 처리
"username": username,
"email": email,
"kakao_id": kakao_id
})
return templates.TemplateResponse("memos.html", {"request": request, "user_info": user})
※ 카카오의 응답 구조는 부분 중첩형으로, id는 최상위에 있지만, email, nickname은 kakao_account 내부에 있다.
{
"id": 123456789,
"kakao_account": {
"email": "abc@kakao.com",
"profile": {
"nickname": "홍길동"
}
}
}
따라서, kakao_account 라는 변수를 추가해 nickname 과 email을 kakao_account 라는 객체 내에서 가져올 수 있도록 한다.
# 사용자 정보 처리
user_info = userinfo_res.json()
kakao_account = user_info.get("kakao_account", {})
profile = kakao_account.get("profile", {})
username = profile.get("nickname", "User")
email = kakao_account.get("email")
kakao_id = user_info.get("id")
◆ controllers.py
controllers.py에 기존 구글 로그인 함수를 수정하여 이 코드를 하나의 함수로 통합하면서, 소셜 로그인 채널을 기준으로 구분(구글, 카카오, 네이버 등)해서 사용자 정보를 처리하도록 만든다.
# 소셜 로그인 후 사용자 정보 처리
def create_or_update_social_user(db: Session, user_info: dict, provider: str):
# provider 별로 ID 및 이름 필드 설정
social_id_filed = f"{provider}_id"
user_name = (
user_info.get('user_name') or
user_info.get('profile_nickname') or
user_info.get('name')
)
social_id_value = user_info.get(social_id_filed)
if not user_info.get('email') or not social_id_value:
raise ValueError('email 또는 소셜 ID가 제공되지 않았습니다.')
# 기존 사용자 조회
user = db.query(User).filter(
(User.email == user_info['email']) | (getattr(User, social_id_filed) == social_id_value)).first()
if not user:
# 신규 사용자 생성
user = User(
username=user_info.get('username'),
email=user_info['email'],
)
setattr(user, social_id_filed, social_id_value)
db.add(user)
else:
# 기존 사용자 정보 업데이트
user.username = user_name or user.username # 이름이 없으면 업데이트 하지 않음
setattr(user, social_id_filed, social_id_value)
db.merge(user) # 업데이트 후 세션에 반영
db.commit()
db.refresh(user)
return user
그리고, google.py와 kakao.py에 변경된 함수가 잘 적용될 수 있도록 코드를 일부 수정해 준다.
[google.py]
from controllers import create_or_update_social_user
@router.get("/auth/google/callback")
async def callback(request: Request, db: Session = Depends(get_db)): # get_db() 사용
code = request.query_params.get("code")
async with httpx.AsyncClient() as client:
token_res = await client.post(GOOGLE_TOKEN_URL, data={
'code': code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'
})
if token_res.status_code != 200:
return {"error": "Failed to retrieve token"}
access_token = token_res.json().get("access_token")
# 사용자 정보 요청
userinfo_res = await client.get(GOOGLE_USERINFO_URL, headers={
'Authorization': f'Bearer {access_token}'
})
if userinfo_res.status_code != 200:
return {"error": "Failed to retrieve user information"}
# 사용자 정보 처리
user_info_raw = userinfo_res.json()
user_info = {
"username" : user_info_raw.get("name") or "User", # 이름이 없을 경우 기본값 설정
"email" : user_info_raw.get("email"),
"google_id" : user_info_raw.get("id")
}
user = create_or_update_social_user(db, user_info, provider='google')
return templates.TemplateResponse("memos.html", {"request": request, "user_info": user})
사용자 정보 처리 부분을 변경해 주는데, user_info를 딕셔너리로 변경하고, 아래에 user 변수에 앞서 만든 create_or_update_social_user 함수를 추가해 provider에 google을 추가해 준다.
[kakao.py]
from controllers import create_or_update_social_user
# 사용자 정보 처리
user_info_raw = userinfo_res.json()
user_info = {
"username": user_info_raw.get("properties", {}).get("nickname") or "User",
"email": user_info_raw.get("kakao_account", {}).get("email"),
"kakao_id": str(user_info_raw.get("id")) # 문자열로 형변환
}
kakao.py 파일도 비슷하나, kakao_id의 경우 숫자로 저장되기 때문에, 이를 문자열로 형변환하여 저장되도록 수정해 준다.
역시 DB에 자동으로 연동될 수 있도록 Alembic을 활용하여 새로운 마이그레이션 파일을 생성하고 적용해 준다.
# 마이그레이션 생성(User 테이블에 kakao_id 칼럼 추가)
alembic revision --autogenerate -m "Add kakao_id to User"
# 마이그레이션 적용
alembic upgrade head
마찬가지로, SQL Shell을 활용하여 User 테이블에 kakao_id 칼럼이 잘 생성되었는지 확인
naver_id 까지 잘 생성되었다.
그리고 home.html 에 카카오 로그인 버튼을 생성해줘야 한다. (구글 로그인 버튼 밑)
※ 추후 네이버 로그인 기능도 추가할 것이므로, 네이버도 추가해 준다.
◆ home.html
<div class="buttons">
<button onclick="window.location.href='/auth/google/login'">Google 계정으로 로그인</button>
</div>
<div class="buttons">
<button onclick="window.location.href='/auth/kakao/login'">KAKAO 계정으로 로그인</button>
</div>
<div class="buttons" style="margin-bottom: 2rem;">
<button onclick="window.location.href='/auth/naver/login'">NAVER 계정으로 로그인</button>
</div>
</form>
그리고 models.py에서 소셜 로그인 시 DB에 연동되기 위해 카카오 ID와 네이버 ID 칼럼을 추가해 준다.
# 사용자 모델 정의
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, index=True) # 정수형, PK
username = Column(String(100), unique=True, index=True, nullable=True) # 소셜 로그인 시 자동 설정 가능
email = Column(String(200), unique=True) # 이메일은 유일해야 함
hashed_password = Column(String(512), nullable=True) # 비밀번호는 선택적
google_id = Column(String(100), unique=True, index=True, nullable=True) # 구글 계정의 고유 ID
kakao_id = Column(String(100), unique=True, index=True, nullable=True) # 카카오 계정의 고유 ID
naver_id = Column(String(100), unique=True, index=True, nullable=True) # 네이버 계정의 고유 ID
이제 main.py에서 카카오와 네이버 라우터를 포함시켜 준다.
◆ main.py
from oauth import google
from oauth import kakao
from oauth import naver
# 라우터 포함
app.include_router(router)
app.include_router(google.router) # Google OAuth2 라우터 포함
app.include_router(kakao.router) # KAKAO OAuth2 라우터 포함
app.include_router(naver.router) # NAVER OAuth2 라우터 포함
※ naver 로그인은 아직 구현되지 않았으므로, 에러 발생 방지를 위해 주석 처리 해놓고 테스트해야 한다.
이제 main.py를 실행하여 서버를 실행한 후, 카카오 로그인 테스트를 해 본다.
home.html에 버튼이 잘 적용된 것을 확인할 수 있다.
[KAKAO 계정으로 로그인] 버튼을 누르면 동의 부분이 뜨고,
메모 앱에 로그인이 잘 된 것을 확인할 수 있다.
[참고]
https://github.com/jintaekimmm/fastapi-simple-auth/blob/main/docs/KAKAO_OAUTH.md
다음 내용
[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(네이버)
이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(카카오)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(구글)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 8: MV
puppy-foot-it.tistory.com
'[파이썬 Projects] > <파이썬 웹개발>' 카테고리의 다른 글
[파이썬] FastAPI - 메모 앱 프로젝트 10: 환영 이메일 발송 (0) | 2025.05.12 |
---|---|
[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(네이버) (0) | 2025.05.12 |
[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(구글) (0) | 2025.05.10 |
[파이썬] FastAPI - 메모 앱 프로젝트 8: MVC 패턴 (1) | 2025.05.10 |
[파이썬] FastAPI - 메모 앱 프로젝트 7: 보완(회원가입 등) (0) | 2025.05.10 |