[파이썬 Projects]/<파이썬 웹개발>

[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(구글)

기록자_Recordian 2025. 5. 10. 22:26
728x90
반응형
이전 내용
 

[파이썬] FastAPI - 메모 앱 프로젝트 8: MVC 패턴

이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 7: 보완(회원가입 등)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 6: 프론트엔드 페이지 개선이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 5: 웹페이

puppy-foot-it.tistory.com


로그인에 소셜 로그인 추가
: 구글, 네이버, 카카오

 

FastAPI에 소셜 로그인을 구현하는 과정은 구글, 네이버, 카카오와 같은 여러 서비스의 OAuth2 API와 통신하여 사용자를 인증하는 것이다.

 

OAuth2 관련한 자세한 내용은 하단 링크 확인

 

[Java] Spring Boot: 스프링 시큐리티, OAuth2, JWT

이전 내용 [Java] Spring Boot: 네이버 로그인 구현하기이전 내용 [Java] Spring Boot: 카카오 로그인 기능 추가하기이전 내용 [Java] Spring Boot: 구글 로그인 기능 추가하기이전 내용 전반적인 순서1. Gradle 또

puppy-foot-it.tistory.com

 

[전체 흐름 요약]

  1. OAuth 제공자(Google, Naver, Kakao)에 앱 등록
  2. Client ID / Secret을 발급받음
  3. FastAPI에서 OAuth2 인증 요청을 보냄
  4. 콜백 URL에서 Authorization Code 수신
  5. 이 코드를 이용해 Access Token 발급
  6. Access Token으로 사용자 정보 조회

1,2번의 경우 이전에 Spring Boot를 통해 OAuth2를 구현한 과정이 있으므로, 자세한 사항은 하단 링크 확인

* 구글 (https://cloud.google.com/cloud-console)

 

[Java] Spring Boot: 구글 로그인 기능 추가하기

이전 내용 [Java] Spring Boot: 스프링 시큐리티, OAuth2, JWT이전 내용 [Java] Spring Boot: 네이버 로그인 구현하기이전 내용 [Java] Spring Boot: 카카오 로그인 기능 추가하기이전 내용 [Java] Spring Boot: 구글 로그

puppy-foot-it.tistory.com

* 카카오

 

[Java] Spring Boot: 카카오 로그인 기능 추가하기

이전 내용 [Java] Spring Boot: 구글 로그인 기능 추가하기이전 내용 전반적인 순서1. Gradle 또는 Maven 의존성 추가2. 구글 API Console 설정 - 구글 개발자 콘솔 프로젝트 생성 → OAuth 2.0 클라이언트 ID 생성

puppy-foot-it.tistory.com

* 네이버

 

[Java] Spring Boot: 네이버 로그인 구현하기

이전 내용 [Java] Spring Boot: 카카오 로그인 기능 추가하기이전 내용 [Java] Spring Boot: 구글 로그인 기능 추가하기이전 내용 전반적인 순서1. Gradle 또는 Maven 의존성 추가2. 구글 API Console 설정 - 구글 개

puppy-foot-it.tistory.com


FastAPI에서 OAuth2 구현
1. 구글

 

◆ 필요한 라이브러리 설치

pip install python-dotenv httpx authlib

 

1. python-dotenv
이 라이브러리는 .env 파일을 읽어 환경 변수를 로드하게 해준다. 주로 비밀번호나 API 키와 같은 민감한 정보를 소스 코드에서 분리하여 관리하도록 도와준다.

  • .env 파일에 정의된 키-값 쌍을 파이썬 환경 변수로 설정.
  • 코드에서 불필요하게 민감정보를 하드코딩하는 것을 방지하여 보안 강화.

2. httpx
httpx는 비동기 HTTP 클라이언트 라이브러리로, 강력한 기능과 유연성을 제공한다. requests 라이브러리의 비동기 버전.

  • 동기 및 비동기 요청을 모두 지원하여, 웹 API 호출 시 효율적으로 작업 처리.
  • 요청에 대한 응답을 쉽게 처리할 수 있는 다양한 기능 제공.

3. authlib
authlib는 OAuth1, OAuth2 및 기타 인증 메커니즘을 처리하기 위한 다양한 도구를 제공하는 라이브러리다. FastAPI와 함께 사용하면 쉽게 OAuth2 인증 기능을 추가할 수 있다.

  • OAuth2 클라이언트, 서버 및 JWT 지원 등을 포함하여 인증을 구현하는 데 유용한 여러 기능 제공.
  • 다양한 인증 프로토콜을 간단하게 설정하고 사용할 수 있게 도와준다.

 환경 변수 설정

OAuth2 클라이언트 ID 및 비밀 키와 같은 민감한 정보를 .env 파일에 저장

[.env 예시]

GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback

 

oauth 디렉터리 생성 후 google.py 생성

oauth 디렉터리를 생성한 후, 해당 디렉터리 내에 google.py 파일을 만든 뒤 OAuth2 기본 로직 구성

import os
import httpx
from fastapi import APIRouter, Request
from dotenv import load_dotenv
from fastapi.templating import Jinja2Templates

load_dotenv()
router = APIRouter()
templates = Jinja2Templates(directory="templates")

GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo"

CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")

@router.get("/auth/google/login")
def login():
    return {
        "auth_url": f"{GOOGLE_AUTH_URL}?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope=openid%20email%20profile"
    }

@router.get("/auth/google/callback")
async def callback(request: Request):
    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 = userinfo_res.json()
        return templates.TemplateResponse("memos.html", {"request": request, "user_info": user_info})

 

◆ main.py

main.py 파일에 astAPI 애플리케이션에 Google 인증을 위한 google 라우터를 추가해 준다.

from oauth import google

app.include_router(google.router)

 

◆ home.html에 구글 로그인 버튼 추가

<div class="buttons">
    <button onclick="window.location.href='/auth/google/login'">Google 계정으로 로그인</button>
</div>

아래의 사진처럼 로그인 버튼 밑에 추가해 주면 된다. (정확한 경로는 '/auth/google/login' 이다.)

 

서버를 실행하여 확인해 보니, 새로운 버튼과 밑의 '사용자 이름:' 이 너무 가깝기 때문에 여백을 지정할 필요가 있다.

 

<div class="buttons" style="margin-bottom: 2rem;">
    <button onclick="window.location.href='/auth/google/login'">Google 계정으로 로그인</button>
</div>

 

간격이 잘 벌어졌다.

 

서버를 실행한 뒤 [Google 계정으로 로그인] 버튼을 누르면 구글 계정을 선택할 수 있게 된다.

그러나, 로그인을 하면 아래와 같은 에러가 발생한다.

◆ models.py 수정

현재의 User 모델은 이미 사용자 인증과 관련된 필드를 갖추고 있지만, 구글 OAuth2를 통해 인증받는 경우에 필요한 정보를 추가할 수 있다. (구글 계정 필드 추가)

# 사용자 모델 정의
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

# Memo 모델 정의
class Memo(Base):
    __tablename__ = 'memo'
    id = Column(Integer, primary_key=True, index=True)  # 정수형, PK 설정
    user_id = Column(Integer, ForeignKey('users.id'))  # 사용자 참조 추가
    title = Column(String(100), nullable=False)  # 제목
    content = Column(String(1000), nullable=False)  # 내용

    user = relationship("User")  # 사용자와의 관계 설정
  • 기존의 hashed_password 필드는 구글 로그인을 사용할 경우 불필요할 수 있으니, 사용자가 구글 계정으로만 로그인할 경우 이 필드를 nullable=True로 설정한다. 구글 계정으로 로그인을 사용할 때는 이메일을 통해 확인할 수 있으니 비밀번호를 요구하지 않을 수도 있다. 구글이 아닌 일반 회원가입 시에는 비밀번호가 해싱된다.
  • 구글 로그인을 사용할 때 username 필드를 자동으로 설정 된다.

◆ google.py 수정

구글 로그인 시 username 필드를 자동으로 설정하기 위해, 사용자 정보를 가져오는 로직에 추가적인 처리가 필요하다.

from fastapi import APIRouter, Request, Depends # Depends 추가
from sqlalchemy.orm import Session # 추가
from controllers import create_or_update_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 = userinfo_res.json()
        username = user_info.get("name") if user_info.get("name") else "User"  # 이름이 없을 경우 기본값 설정
        email = user_info.get("email")
        google_id = user_info.get("id")

        user = create_or_update_user(db, {  # db를 사용하여 사용자 정보 처리
            "username": username,
            "email": email,
            "google_id": google_id
        })

        return templates.TemplateResponse("memos.html", {"request": request, "user_info": user})

 

◆ controllers.py 수정

사용자 정보를 저장하는 함수 create_or_update_user()를 추가해 준다. 이 함수는 사용자가 존재하면 업데이트하고, 존재하지 않으면 새로 생성하는 로직이다.

# 구글 로그인 후 사용자 정보 처리
def create_or_update_user(db: Session, user_info: dict):
    user = db.query(User).filter(
        (User.email == user_info['email']) | (User.google_id == user_info['google_id'])
    ).first()

    if not user:
        # 신규 사용자 생성
        user = User(
            username=user_info.get('username'),
            email=user_info['email'],
            google_id=user_info['google_id']
        )
        db.add(user)
    else:
        # 기존 사용자 정보 업데이트
        user.username = user_info.get('username') or user.username  # 이름이 없으면 업데이트 하지 않음
        user.google_id = user_info['google_id']  # 구글 ID는 업데이트
        db.merge(user)  # 업데이트 후 세션에 반영

    db.commit()
    db.refresh(user)
    return user

 

◆ Alembic 설정

모델을 수정한 후, 새로운 칼럼이 추가되었기 때문에 Alembic을 사용하여 새로운 마이그레이션 파일을 생성

alembic revision --autogenerate -m "Add google_id to User"

 

ModuleNotFoundError: No module named 'httpx' 오류가 계속 발생해서 확인해 보니, 패키지 간의 파이썬 버전 충돌 문제로 httpx 0.24.0 버전은 파이썬 3.8버전부터 사용할 수 있는데, 필자가 사용하던 버전이 3.7.* 버전이라 3.11의 다른 가상환경으로 실행한 뒤, requirements.txt에 있는 파일을 다시 불러와 설치해 주었다.

pip install -r requirements.txt

 

그리고 다른 환경(PC)에서도 사용할 수 있게 requirements.txt를 업데이트해줬다.

pip freeze > requirements.txt

 

그리고 나서 Alembic을 사용하여 마이그레이션 파일을 생성하니 잘 된다.

 

그리고 마이그레이션을 적용하는 명령어를 입력하면

alembic upgrade head

 

memo_app 데이터베이스의 users 테이블에 google_id 칼럼이 잘 생성되었는지 확인해 본다.

-- 데이터베이스 목록 보기
\l

-- memo_app 데이터베이스 사용하기
\c memo_app;

-- 테이블 목록 보기
\dt

-- users 테이블의 칼럼 보기
SELECT * FROM users;

 

google_id 칼럼이 잘 생성된 것을 확인할 수 있다.

 

다시 main.py 파일을 실행하고 서버를 실행하여 구글 계정 로그인을 진행하면 200 OK 응답을 받으며 로그인 페이지 URL을 반환한다.

 

◆ google.py 수정

현재 구글 계정 로그인을 하게 되면, 인증 URL을 JSON 값으로 반환한 할 뿐, memos.html로 이동하지 않기 때문에 해당 부분을 수정하여 구글 계정 로그인 성공 시 memos.html로 이동하도록 수정한다.

# 기존 코드
@router.get("/auth/google/login")
def login():
    authn {
        "auth_url": f"{GOOGLE_AUTH_URL}?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope=openid%20email%20profile"
    }

 

fastapi.responses.RedirectResponse를 사용하면 유저의 브라우저를 자동으로 구글 로그인 페이지로 이동시킬 수 있다.

from fastapi.responses import RedirectResponse

@router.get("/auth/google/login")
def login():
    auth_url = (
        f"{GOOGLE_AUTH_URL}?response_type=code"
        f"&client_id={CLIENT_ID}"
        f"&redirect_uri={REDIRECT_URI}"
        f"&scope=openid%20email%20profile"
    )
    return RedirectResponse(url=auth_url)

 

코드를 변경하고, 서버를 다시 실행하여 [구글 계정으로 로그인] 버튼을 클릭하면, 구글 로그인이 잘 구현된다.

 


[참고]

https://datamoney.tistory.com/382

https://brotherdan.tistory.com/47

https://velog.io/@da_na/OAuth2-%EA%B5%AC%EA%B8%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%97%B0%EB%8F%99-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EC%B1%85


다음 내용

 

[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(카카오)

이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(구글)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 8: MVC 패턴이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 7: 보완(회원가입 등)

puppy-foot-it.tistory.com

728x90
반응형