[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(구글)
이전 내용
[파이썬] 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
[전체 흐름 요약]
- OAuth 제공자(Google, Naver, Kakao)에 앱 등록
- Client ID / Secret을 발급받음
- FastAPI에서 OAuth2 인증 요청을 보냄
- 콜백 URL에서 Authorization Code 수신
- 이 코드를 이용해 Access Token 발급
- 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
다음 내용
[파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(카카오)
이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 9: 소셜 로그인 추가(구글)이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 8: MVC 패턴이전 내용 [파이썬] FastAPI - 메모 앱 프로젝트 7: 보완(회원가입 등)
puppy-foot-it.tistory.com