728x90
반응형
SMALL
안녕하세요! 이번 포스팅에서는 FastAPI 애플리케이션에 **인증(Authentication)**과 권한 관리(Authorization) 기능을 추가하여 API 보안을 한층 강화하는 방법을 다루어보겠습니다. 현대 웹 서비스에서는 단순한 CRUD만으로는 충분하지 않으며, 사용자 식별과 권한 부여가 반드시 필요합니다.
특히 JWT(JSON Web Token)를 이용해 토큰 기반 인증을 구현하고, OAuth2 흐름을 적용함으로써 더욱 안전하고 유연한 인증 시스템을 구성할 수 있습니다. 이번 장을 통해 로그인/로그아웃, 사용자 역할(Role) 설정 등 핵심 인증 로직을 차근차근 익혀봅시다.
5.1. JWT 개념과 작동 원리
5.1.1. JWT(JSON Web Token)란?
- JWT는 인증 정보를 JSON 형태로 담아, 서버-클라이언트 간 인증을 간편하고 확장성 있게 처리하도록 고안된 토큰 형식입니다.
- 크게 3부분(헤더.페이로드.서명)으로 구성되어 있습니다.
- Header: 알고리즘 타입(HS256 등)을 명시
- Payload: 실제 인증 정보를 담는 부분 (예: sub, exp, iat, roles)
- Signature: 헤더와 페이로드를 합쳐 비밀키로 서명한 값
5.1.2. JWT 인증의 장점
- Stateless: 세션 정보를 서버에 저장할 필요가 없으므로, 서버 부하가 줄어듭니다.
- 확장성: 여러 서비스 간 공유가 용이해, 마이크로서비스나 외부 API 연동에도 자주 사용됩니다.
- 자체 검증: 유효 기간(exp)이나 서명 검증을 통해, 서버가 직접 유효성을 판단합니다.
5.2. FastAPI에서의 OAuth2 Password Flow
5.2.1. OAuth2 Password Flow란?
- OAuth2는 다양한 인증 흐름(Flow)을 정의한 표준이며, Password Flow는 “사용자 이름/비밀번호”를 직접 받고 토큰을 발급하는 가장 간단한 방식입니다.
- FastAPI는 이 흐름을 지원하기 위한 유틸을 내장(OAuth2PasswordBearer, OAuth2PasswordRequestForm)하고 있습니다.
5.2.2. 인증 로직 개요
- 로그인 요청: 클라이언트가 /login 엔드포인트로 이메일/비밀번호를 전송 (POST)
- 사용자 검증: DB에서 사용자 정보를 조회하여 비밀번호 일치 여부 확인
- JWT 토큰 발급: 검증 성공 시, 만료 시간(exp) 등을 포함한 Access Token을 생성
- API 접근 시 인증 헤더 전송: 클라이언트는 Authorization: Bearer <token> 헤더로 토큰을 전달
- 서버는 토큰 검증: 만료 여부, 서명(Signature) 확인, 필요한 경우 사용자 권한(Role)도 확인
5.3. 의존성 설치 및 환경 설정
5.3.1. Poetry로 라이브러리 설치
poetry add python-jose passlib[bcrypt]
- python-jose: JWT 토큰을 인코딩/디코딩하기 위한 라이브러리
- passlib[bcrypt]: 비밀번호 해싱 및 검증을 위한 라이브러리
참고: bcrypt 이외에도 argon2 등 다양한 해시 알고리즘이 있습니다. 프로젝트 요구사항에 맞게 선택하세요.
5.3.2. 환경 변수 설정 (.env)
JWT_SECRET_KEY=SUPER_SECRET_KEY
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
- 실제 서비스에서는 .env 파일을 Git에 커밋하지 않거나, Secrets Manager를 사용하여 안전하게 관리해야 합니다.
5.4. JWT 토큰 생성 및 검증
5.4.1. 유틸 함수 (core/security.py 예시)
# app/core/security.py
import os
from datetime import datetime, timedelta
from jose import JWTError, jwt
SECRET_KEY = os.getenv("JWT_SECRET_KEY")
ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30))
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_access_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload # payload 내에 sub, exp 등 정보가 들어 있음
except JWTError:
return None
- create_access_token: 사용자 식별 정보(sub 또는 user_id)와 만료 시간을 페이로드에 담아 JWT 문자열을 생성
- verify_access_token: 토큰이 유효한지(서명, 만료 시간 등) 확인 후, 정상이라면 페이로드 반환
주의: 실제 서비스에서는 만료 시간, 리프레시 토큰, 키 로테이션 등 보안을 강화해야 합니다.
5.4.2. 비밀번호 해싱 (passlib 활용)
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
- 사용자 입력 비밀번호는 절대 평문으로 저장하지 말고, 해시(단방향 암호화)한 뒤 DB에 저장해야 합니다.
- 로그인 시에는 verify_password를 사용해 입력 비밀번호가 해시와 일치하는지 검사합니다.
5.5. 로그인 & 사용자 인증 엔드포인트
5.5.1. 로그인 API 예시
# app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.core.security import create_access_token, verify_password
from app.crud.user import get_user_by_email
from app.schemas.token import Token
router = APIRouter()
@router.post("/login", response_model=Token)
def login(
db: Session = Depends(get_db),
form_data: OAuth2PasswordRequestForm = Depends()
):
# 1) 사용자 찾기
user = get_user_by_email(db, form_data.username)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
# 2) 비밀번호 검증
if not verify_password(form_data.password, user.hashed_password):
raise HTTPException(status_code=400, detail="Incorrect email or password")
# 3) Access Token 발급
access_token = create_access_token(data={"sub": str(user.id)})
return {
"access_token": access_token,
"token_type": "bearer"
}
- OAuth2PasswordRequestForm는 기본적으로 username, password 필드를 지원하며, FastAPI에서 로그인 폼을 처리할 때 자주 사용됩니다. (이 예시에서는 username을 이메일로 활용)
- 성공 시, JWT를 담은 access_token과 token_type을 응답합니다.
5.5.2. Token Pydantic 스키마 (schemas/token.py)
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
5.6. 보호된 엔드포인트 접근
5.6.1. 토큰 검증 의존성 (get_current_user)
# app/api/deps.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError
from sqlalchemy.orm import Session
from app.core.security import verify_access_token
from app.crud.user import get_user_by_id
from app.db.session import SessionLocal
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
payload = verify_access_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
headers={"WWW-Authenticate": "Bearer"},
)
user_id: str = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="Token payload invalid")
user = get_user_by_id(db, int(user_id))
if not user:
raise HTTPException(status_code=401, detail="User not found")
return user
- OAuth2PasswordBearer: FastAPI가 Authorization: Bearer <token> 헤더로부터 토큰을 추출해줍니다.
- verify_access_token: 서명 검증, 만료 시간 검증 수행. 무효하면 401 Unauthorized 예외 발생.
- get_user_by_id: 유효한 토큰이면 실제 DB에서 사용자 정보를 가져옵니다. 존재하지 않으면 여전히 401로 처리.
5.6.2. 보호된 API 사용 예시
# app/api/v1/endpoints/user.py (일부 예시)
from fastapi import APIRouter, Depends
from app.models.user import User
from app.api.deps import get_current_user
router = APIRouter()
@router.get("/me")
def read_current_user(current_user: User = Depends(get_current_user)):
return {
"id": current_user.id,
"email": current_user.email,
"role": current_user.role
}
- 이 엔드포인트는 JWT 토큰이 필요하므로, 미인증 상태에서 호출하면 401 에러가 발생합니다.
- 토큰을 헤더에 담아 보낸다면, 인증을 통과해 current_user 정보를 얻을 수 있습니다.
5.7. 권한(Role) 관리
5.7.1. Role 필드와 접근 제한
- 사용자 테이블 혹은 스키마에 role 필드를 둡니다. 예: "user", "admin", "manager" 등.
- 인증만으로는 “누구냐?”를 확인할 수 있지만, “무엇을 할 수 있느냐?”(권한)는 role 등에 따라 세밀하게 제어합니다.
5.7.2. 예시: 관리자 전용 API 데코레이터
# app/api/deps.py
from fastapi import HTTPException, status
def get_admin_user(current_user: User = Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user
# app/api/v1/endpoints/admin.py
from fastapi import APIRouter, Depends
from app.models.user import User
from app.api.deps import get_admin_user
router = APIRouter()
@router.get("/stats")
def get_admin_stats(admin_user: User = Depends(get_admin_user)):
# 관리자만 접근 가능한 통계 API
return {"data": "Some sensitive stats..."}
- 403 Forbidden을 활용해 “인증은 되었지만, 권한이 없는” 경우를 구분합니다.
- 운영 환경에서 종종 관리자 권한(또는 여러 Role)을 분기 처리해 특별 액션을 수행합니다.
5.8. 실제 활용 사례
5.8.1. 전자상거래(E-Commerce)
- 회원 가입 시 비밀번호를 bcrypt 해시로 저장
- 로그인 시 JWT 발급, 주문/결제/장바구니 API 접근 시 Bearer 토큰 필수
- 관리자 Role은 상품 등록, 재고 관리, 매출 통계 페이지에 접근 가능
5.8.2. 사내 업무 시스템
- 직원은 본인 정보만 수정 가능, 인사팀(HR Role)은 모든 직원 정보 수정 가능
- JWT는 만료 시간이 짧게 설정되어 보안 강화
- 사내 SSO와 연동해 Role 정보를 한 번에 가져오는 경우도 있음
5.8.3. 마이크로서비스 아키텍처
- API Gateway에서 먼저 JWT 인증을 수행하고, 유효한 요청만 마이크로서비스로 전달
- Role이 여러 개일 수도 있으며, 토큰에 권한(Scopes) 정보를 담아 각 마이크로서비스에서 해석
- Redis나 다른 캐시를 사용해 로그아웃 처리(JWT 블랙리스트) 로직을 구현하기도 함
모범 사례
- 비밀번호는 항상 안전한 알고리즘(bcrypt, Argon2 등)으로 해시
- 만료 시간을 명확히 설정하고, 리프레시 토큰을 통해 재발급 체계 마련
- HTTPS 전송: JWT는 평문으로 중요한 정보를 담으므로, HTTPS가 필수
- 민감 정보(private_claims)는 가급적 토큰에 담지 않거나, 암호화/최소화
- 사용자 로깅, 감사 기록(Audit Trail)을 통해 무단 접근 시도를 추적
5.9. 마무리
이번 5장에서는 FastAPI 애플리케이션에 JWT 기반 인증을 구현하고, 권한(Role)에 따라 접근을 제어하는 방법을 배웠습니다. 구체적으로:
- JWT의 개념과 작동 원리를 이해하고
- OAuth2 Password Flow를 활용해 로그인/토큰 발급 과정을 구성했으며
- Bearer 토큰을 검증해 인증/인가를 처리하는 실전 코드를 작성했습니다.
- 권한 관리를 통해 관리자 전용 API 등 세분화된 접근 제어를 실습해봤습니다.
다음 장에서는 테스트 및 디버깅에 대해 좀 더 구체적으로 알아볼 예정입니다. 인증/권한 관련 로직은 오류가 발생하면 서비스 전반에 치명적 영향을 줄 수 있으므로, 충분한 테스트가 매우 중요합니다. 실무에서도 꼭 Pytest와 CI 환경을 구축하여 자동화된 테스트로 문제를 조기에 발견하시길 권장드립니다!
728x90
반응형
LIST
'소프트웨어 개발 > 백엔드' 카테고리의 다른 글
📚[FastAPI] 7장. 비동기 작업 및 배포: Celery, Docker로 확장성 높이기 (0) | 2025.01.26 |
---|---|
📚[FastAPI] 6장. 테스트 및 디버깅: Pytest 활용과 품질 보증 (0) | 2025.01.26 |
📚[FastAPI] 4장. API 설계 및 구현: RESTful 엔드포인트와 Pydantic 스키마 (0) | 2025.01.26 |
📚[FastAPI] 3장. 데이터베이스 연동: SQLAlchemy와 Alembic으로 CRUD 구축하기 (1) | 2025.01.26 |
📚[FastAPI] 2장. FastAPI 기본 구조 설계: 디렉토리 구성과 라우팅 전략 (0) | 2025.01.26 |