
FastAPI JWT 인증을 만들 때 가장 먼저 헷갈리는 지점은 토큰 검증 코드를 어디에 둘지입니다. Depends에 둘 수도 있고, middleware에 둘 수도 있고, router 단위로 보호할 수도 있습니다.
좋은 기준은 인증 정보를 필요한 엔드포인트에 명확히 전달하면서 공개 API와 보호 API의 경계를 흐리지 않는 것입니다. JWT 검증 자체보다 이 책임 분리가 유지보수를 크게 좌우합니다.

FastAPI JWT 인증 흐름을 먼저 나눠 보기
JWT 인증은 보통 로그인, access token 발급, 보호 API 호출, 토큰 검증, 사용자 조회 순서로 흐릅니다. 이때 모든 코드를 하나의 middleware에 넣으면 처음에는 편하지만, 예외 처리와 라우터별 정책이 커질수록 복잡해집니다.
1. POST /auth/login
id/password 확인 후 access token 발급
2. GET /me
Authorization: Bearer <token>
3. token decode
subject, expire, scope 확인
4. current_user 조회
endpoint에서 사용자 정보 사용OAuth2PasswordBearer는 무엇을 해줄까
FastAPI의 OAuth2PasswordBearer는 Authorization 헤더에서 Bearer 토큰을 읽는 dependency입니다. 이 객체가 JWT를 완전히 검증해주는 것은 아닙니다. 토큰 문자열을 꺼내고, 그 다음 검증은 직접 구현해야 합니다.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
async def get_current_user(token: str = Depends(oauth2_scheme)):
payload = decode_access_token(token)
user = await find_user(payload["sub"])
return user이 구조의 장점은 endpoint가 필요한 사용자 정보를 Depends로 받는다는 점입니다. 함수 시그니처만 봐도 이 API가 인증된 사용자를 필요로 한다는 사실이 드러납니다.
Depends에 두기 좋은 책임
- Authorization 헤더에서 토큰 읽기
- JWT 서명과 만료 시간 검증
- 토큰 subject로 사용자 조회
- 권한 scope나 role 확인
- 검증 실패 시 401 또는 403 예외 발생
Depends는 특정 endpoint나 router에 필요한 인증 요구사항을 붙이기 좋습니다. 공개 API에는 붙이지 않고, 보호 API에만 붙이면 경계가 분명합니다.
middleware에 넣기 좋은 책임
middleware는 모든 요청에 공통으로 적용되는 처리에 잘 맞습니다. 요청 id 생성, 로깅, CORS, 처리 시간 측정처럼 인증 여부와 무관하게 공통인 작업이 대표적입니다.
인증도 middleware에서 할 수는 있지만 모든 route에 강제로 걸리기 쉽고, 공개 endpoint 예외 처리 목록이 늘어날 수 있습니다.
@app.middleware("http")
async def add_process_time_header(request, call_next):
response = await call_next(request)
response.headers["X-App-Version"] = "1.0"
return responserouter 단위 보호가 필요한 이유
API가 커지면 공개 router와 보호 router를 나누는 편이 이해하기 쉽습니다. 예를 들어 auth router는 로그인과 토큰 재발급을 담당하고, account router는 current_user를 요구하도록 만들 수 있습니다.
protected_router = APIRouter(
prefix="/account",
dependencies=[Depends(get_current_user)],
)다만 router-level dependency는 반환값을 endpoint 인자로 직접 받기 어렵습니다. 사용자 객체가 실제로 필요하다면 endpoint 인자에 Depends를 명시하는 방식이 더 읽기 쉽습니다.
current_user dependency 예시
@router.get("/me")
async def read_me(
current_user: User = Depends(get_current_user),
):
return {
"id": current_user.id,
"email": current_user.email,
}이 구조는 테스트에서도 유리합니다. FastAPI의 dependency override를 사용하면 get_current_user만 바꿔 보호 API 테스트를 단순화할 수 있습니다.
access token과 refresh token 경계
access token은 짧게 유지하고 API 인증에 씁니다. refresh token은 access token을 다시 발급받는 데 쓰며, 저장 위치와 폐기 정책을 더 조심해야 합니다.
- access token: 보호 API 호출에 사용
- refresh token: 새 access token 발급에 사용
- 로그아웃 또는 탈취 대응: refresh token 저장소와 폐기 정책 필요
- 권한 변경 반영: access token 만료 시간과 재검증 정책 검토
예외 응답을 일관되게 유지하기
인증 실패는 401, 권한 부족은 403으로 나누는 편이 클라이언트가 대응하기 쉽습니다. 토큰이 없거나 만료된 경우와, 로그인은 되었지만 권한이 부족한 경우는 다른 문제입니다.
from fastapi import HTTPException, status
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)실무 설계 순서
- 공개 router와 보호 router를 먼저 나눈다
- 토큰 읽기는 OAuth2PasswordBearer로 시작한다
- JWT decode와 사용자 조회는 get_current_user로 모은다
- 권한 정책은 get_current_admin 같은 별도 dependency로 분리한다
- middleware에는 인증 세부 로직보다 공통 관찰/로깅 책임을 둔다
정리
FastAPI JWT 인증은 토큰을 검증하는 코드보다 책임을 어디에 둘지가 더 중요합니다. Depends는 사용자 정보 주입과 보호 API 표현에 좋고, middleware는 모든 요청 공통 처리에 더 잘 맞습니다.
기본 보안 흐름은 FastAPI OAuth2PasswordBearer 문서를 기준으로 확인할 수 있습니다. DB migration 흐름은 Alembic autogenerate 글과 함께 보면 좋습니다.