Skip to content

인증 (세션)

인증이란 무엇인가?

인증(Authentication)의 필요성

웹 애플리케이션에서 인증은 필수적입니다:

  1. 사용자 식별 - "당신이 누구인지" 확인하는 과정
  2. 리소스 보호 - 허가된 사용자만 특정 정보에 접근
  3. 개인화 - 사용자별 맞춤 콘텐츠 제공
  4. 보안 - 무단 접근으로부터 시스템 보호
  5. 책임 추적 - 누가 언제 무엇을 했는지 기록

인증의 역할

  • 신원 확인 (Authentication): "당신은 누구입니까?"
  • 권한 부여 (Authorization): "무엇을 할 수 있습니까?"
  • 감사 추적 (Audit Trail): "누가 무엇을 했습니까?"

주요 인증 방식

방식 특징 장점 단점 적합한 사용 예
세션 기반 서버에 상태 저장 즉시 무효화, 복잡한 상태 관리 서버 메모리 사용, 확장성 고려 필요 웹 애플리케이션, 관리자 패널, 전자상거래
JWT Stateless 토큰 서버 부담 없음, 마이크로서비스 친화적 토큰 크기, 무효화 어려움 모바일 앱, SPA, 마이크로서비스 API
OAuth2 제3자 인증 위임 사용자 편의성, 보안 책임 분산 구현 복잡도, 외부 의존성 소셜 로그인, 써드파티 API 연동
API Key 고정 키 사용 간단한 구현 키 관리 어려움, 세밀한 제어 불가 서버 간 통신, 외부 서비스 연동, Webhook
Basic Auth HTTP 기본 인증 매우 간단 보안 취약, 기능 제한적 내부 시스템, 개발 환경, 간단한 API

왜 API에서도 세션 인증인가?

JWT가 유행하는 시대에도 세션 기반 인증은 여전히 강력하고 실용적인 선택입니다:

세션 인증의 장점

  1. 즉시 무효화 - 로그아웃이나 권한 변경이 즉시 반영
  2. 작은 요청 크기 - 쿠키에는 세션 ID만 전송 (JWT는 수백 바이트)
  3. 서버 측 제어 - 동시 로그인 제한, 강제 로그아웃 가능
  4. 복잡한 상태 관리 - 장바구니, 임시 데이터 등 저장 용이
  5. 보안 - 민감한 정보가 클라이언트에 노출되지 않음

JWT의 한계

  • 토큰 무효화 어려움 (블랙리스트 관리 필요)
  • 토큰 크기가 커서 매 요청마다 오버헤드
  • 클레임 변경 시 재발급 필요
  • Stateless라 복잡한 시나리오 구현 어려움

FastAPI에서 세션 구현

1. 기본 설정

from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.sessions import SessionMiddleware
from typing import Optional
import secrets

app = FastAPI()

# 세션 미들웨어 추가
app.add_middleware(
    SessionMiddleware,
    secret_key=secrets.token_urlsafe(32),  # 프로덕션에서는 환경변수 사용
    session_cookie="session",
    max_age=3600,  # 1시간
    same_site="lax",
    https_only=False,  # 프로덕션에서는 True
)

2. 로그인/로그아웃 구현

from pydantic import BaseModel
from datetime import datetime

class LoginRequest(BaseModel):
    username: str
    password: str

class User(BaseModel):
    id: int
    username: str
    email: str
    role: str = "user"

# 가상의 사용자 DB
fake_users_db = {
    "user1": {
        "id": 1,
        "username": "user1",
        "email": "user1@example.com",
        "password": "secret123",  # 실제로는 해시 저장
        "role": "admin"
    }
}

@app.post("/api/login")
async def login(request: Request, login_data: LoginRequest):
    """세션 기반 로그인"""
    user = fake_users_db.get(login_data.username)

    if not user or user["password"] != login_data.password:
        raise HTTPException(status_code=401, detail="Invalid credentials")

    # 세션에 사용자 정보 저장
    request.session["user_id"] = user["id"]
    request.session["username"] = user["username"]
    request.session["role"] = user["role"]
    request.session["login_time"] = datetime.now().isoformat()

    return {
        "message": "Login successful",
        "user": {
            "id": user["id"],
            "username": user["username"],
            "role": user["role"]
        }
    }

@app.post("/api/logout")
async def logout(request: Request):
    """세션 종료"""
    request.session.clear()
    return {"message": "Logout successful"}

3. 세션 의존성 주입

async def get_current_user(request: Request) -> Optional[User]:
    """현재 로그인한 사용자 정보 반환"""
    user_id = request.session.get("user_id")
    if not user_id:
        return None

    # 실제로는 DB에서 조회
    for user_data in fake_users_db.values():
        if user_data["id"] == user_id:
            return User(**user_data)
    return None

async def require_user(user: Optional[User] = Depends(get_current_user)):
    """로그인 필수 의존성"""
    if not user:
        raise HTTPException(status_code=401, detail="Not authenticated")
    return user

async def require_admin(user: User = Depends(require_user)):
    """관리자 권한 필수"""
    if user.role != "admin":
        raise HTTPException(status_code=403, detail="Admin access required")
    return user

4. 보호된 API 엔드포인트

@app.get("/api/me")
async def get_me(user: User = Depends(require_user)):
    """현재 사용자 정보"""
    return user

@app.get("/api/admin/users")
async def get_all_users(admin: User = Depends(require_admin)):
    """관리자 전용 - 모든 사용자 조회"""
    return list(fake_users_db.values())

@app.get("/api/session-info")
async def session_info(request: Request):
    """세션 정보 확인 (디버깅용)"""
    return {
        "session_data": dict(request.session),
        "session_id": request.session.get("session_id", "N/A")
    }

고급 기능

1. Redis 세션 저장소

import redis
from fastapi_sessions import SessionCookie, InMemoryBackend
from fastapi_sessions.backends.implementations import RedisBackend

# Redis 백엔드 설정
redis_client = redis.Redis(host="localhost", port=6379, db=0)
backend = RedisBackend(redis_client)

# 또는 aioredis 사용
# import aioredis
# redis_client = aioredis.from_url("redis://localhost")
# backend = RedisBackend(redis_client)

2. 동시 로그인 제어

@app.post("/api/login-exclusive")
async def login_exclusive(request: Request, login_data: LoginRequest):
    """단일 세션만 허용 (기존 세션 무효화)"""
    user = authenticate_user(login_data.username, login_data.password)

    if not user:
        raise HTTPException(status_code=401)

    # 기존 세션 무효화 (Redis 사용 시)
    # await invalidate_user_sessions(user.id)

    # 새 세션 생성
    request.session["user_id"] = user.id
    request.session["session_token"] = secrets.token_urlsafe(32)

    return {"message": "Login successful"}

3. 세션 활동 추적

from datetime import datetime, timedelta

@app.middleware("http")
async def update_session_activity(request: Request, call_next):
    """각 요청마다 세션 활동 시간 업데이트"""
    if "user_id" in request.session:
        request.session["last_activity"] = datetime.now().isoformat()

    response = await call_next(request)
    return response

async def check_session_timeout(request: Request):
    """세션 타임아웃 확인"""
    if "last_activity" in request.session:
        last_activity = datetime.fromisoformat(request.session["last_activity"])
        if datetime.now() - last_activity > timedelta(minutes=30):
            request.session.clear()
            raise HTTPException(status_code=401, detail="Session expired")

프로덕션 고려사항

1. 보안 설정

# .env 파일
SESSION_SECRET_KEY=your-secret-key-here
SESSION_SECURE=true  # HTTPS 전용
SESSION_HTTPONLY=true  # JavaScript를 통한 세션 쿠키 접근 막기
SESSION_SAMESITE=lax  # CSRF 보호

# 설정 적용
from pydantic import BaseSettings

class Settings(BaseSettings):
    session_secret_key: str
    session_secure: bool = True
    session_httponly: bool = True
    session_samesite: str = "lax"

    class Config:
        env_file = ".env"

settings = Settings()

2. 확장성

# 다중 서버 환경에서 Redis 필수
app.add_middleware(
    SessionMiddleware,
    secret_key=settings.session_secret_key,
    session_cookie="session",
    max_age=3600,
    same_site=settings.session_samesite,
    https_only=settings.session_secure,
    # Redis 백엔드 사용으로 서버 간 세션 공유
)

세션 vs JWT 선택 가이드

세션을 선택해야 할 때:

  • ✅ 즉시 로그아웃이 중요한 경우
  • ✅ 복잡한 권한 관리가 필요한 경우
  • ✅ 서버 측 상태 관리가 필요한 경우
  • ✅ 동시 로그인 제어가 필요한 경우

JWT를 선택해야 할 때:

  • ✅ 완전한 Stateless가 필요한 경우
  • ✅ 마이크로서비스 간 인증이 필요한 경우
  • ✅ 서버 리소스가 제한적인 경우
  • ✅ 모바일 앱 등 쿠키 사용이 어려운 경우

결론

세션 인증은 여전히 강력하고 실용적인 선택입니다. 특히 웹 애플리케이션이나 관리자 패널 같은 경우, 세션의 장점이 JWT보다 훨씬 큽니다. Redis와 함께 사용하면 확장성 문제도 해결되며, 더 안전하고 유연한 인증 시스템을 구축할 수 있습니다.

자체 웹서비스인 경우에는 세션을 쓰세요. 세션 사용이 불가능한 상황 (ex: Android/iOS 앱)에서는 JWT를 사용하셔도 됩니다.

Comments