Skip to content

6.3장: 기본 인증

학습 시간: 1시간


1. API 인증: 임무 통제 센터 입장 허가증

인증 — 사용자 신원을 확인하는 과정입니다. 세션 및 쿠키를 사용하는 웹사이트와 달리, 스테이트리스(상태 비저장) API는 일반적으로 토큰을 사용합니다.

과정은 다음과 같습니다:

  1. 사용자는 자신의 로그인 정보와 비밀번호를 특정 엔드포인트(예: /login)로 전송합니다.
  2. 서버는 이를 확인합니다. 모든 것이 올바르면 서버는 고유하게 암호화된 토큰(긴 문자열)을 생성하여 다시 보냅니다.
  3. 보호된 리소스(예: POST /planets)에 대한 후속 요청 시 사용자는 Authorization 헤더에 이 토큰을 첨부해야 합니다.
  4. 서버는 토큰의 유효성을 확인하고, 토큰이 올바르면 요청을 실행합니다.

💡 우주 비유:

  • 로그인/비밀번호 = 출입증 발급을 위한 생체 스캔.
  • 토큰 = 임무 통제 센터 입구에서 받는 전자 출입증(ID 카드).
  • Authorization: Bearer <토큰> 헤더 = 모든 보호된 문에서 리더기에 출입증을 갖다 댑니다.
  • 보호된 엔드포인트 (POST, PUT, DELETE) = 서버실 또는 발사 제어 콘솔로 향하는 문.

2. Laravel에서의 인증: Sanctum

Laravel은 API 인증을 위한 우아한 솔루션인 Laravel Sanctum을 제공합니다. 이는 SPA(단일 페이지 애플리케이션), 모바일 애플리케이션 및 간단한 토큰 기반 API에 이상적입니다.

단계 1: Sanctum 설치 및 구성

Sanctum은 표준 Laravel 애플리케이션에 이미 설치되어 있지만, 구성을 확인해 보겠습니다.

  1. 구성 발행 (아직 하지 않았다면):
    php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
    
  2. 마이그레이션 실행 (personal_access_tokens 테이블 생성):
    php artisan migrate
    
  3. User 모델에 트레이트 추가: app/Models/User.php를 열고 HasApiTokens 트레이트를 사용하는지 확인하십시오.
    // app/Models/User.php
    use Laravel\Sanctum\HasApiTokens;
    
    class User extends Authenticatable
    {
        use HasApiTokens, HasFactory, Notifiable;
        // ...
    }
    

단계 2: 토큰 발급을 위한 엔드포인트 생성 사용자가 로그인/비밀번호를 보낼 라우트가 필요합니다.

routes/api.php에 추가:

// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
use Illuminate\Validation\ValidationException;

Route::post('/login', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['로그인 정보가 잘못되었습니다.'],
        ]);
    }

    // 토큰 반환
    return response()->json([
        'token' => $user->createToken('api-token')->plainTextToken
    ]);
});

테스트를 위해 시더(seeder) 또는 Tinker를 통해 사용자를 생성할 수 있습니다.

단계 3: 라우트 보호 이제 CRUD 작업을 보호하겠습니다. routes/api.php를 변경합니다:

// routes/api.php
use App\Http\Controllers\PlanetController;

// 행성 보기를 위한 공개 라우트
Route::get('/planets', [PlanetController::class, 'index']);
Route::get('/planets/{planet}', [PlanetController::class, 'show']);

// 보호된 라우트 그룹
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/planets', [PlanetController::class, 'store']);
    Route::put('/planets/{planet}', [PlanetController::class, 'update']);
    Route::delete('/planets/{planet}', [PlanetController::class, 'destroy']);

    // 로그아웃(토큰 삭제)을 위한 라우트
    Route::post('/logout', function (Request $request) {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => '성공적으로 로그아웃되었습니다'], 200);
    });
});

미들웨어 auth:sanctumAuthorization 헤더에 유효한 토큰이 있는지 확인할 것입니다.


3. FastAPI에서의 인증: OAuth2 및 JWT

FastAPI에는 내장된 인증 시스템이 없지만, 이를 구현하기 위한 강력한 도구가 있습니다. 사실상의 표준은 JWT 토큰을 사용한 OAuth2입니다.

단계 1: 종속성 설치

pip install "python-jose[cryptography]" "passlib[bcrypt]" "python-multipart"

  • python-jose: JWT 토큰 생성 및 확인용.
  • passlib: 비밀번호 해싱 및 확인용.
  • python-multipart: 폼 데이터(usernamepassword) 처리용.

단계 2: 보안 모듈 생성 (security.py) 모든 인증 로직을 별도의 파일로 분리하는 것이 좋은 방법입니다.

security.py 파일 생성:

# security.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta, timezone

# --- 설정 ---
SECRET_KEY = "your-super-secret-key-that-is-long-and-random" # ⚠️ 본인의 키로 교체하세요!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# --- 유틸리티 ---
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login")

# --- 함수 ---
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
    return pwd_context.hash(password)

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + 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 get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="로그인 정보를 확인할 수 없습니다",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    # 여기서 DB에서 사용자를 반환할 수 있지만, 지금은 단순히 이름을 반환합니다
    return {"username": username}

단계 3: main.py에 통합 이제 이것을 우리 애플리케이션에 연결하겠습니다.

  1. /login 엔드포인트 생성:

    # main.py
    from fastapi.security import OAuth2PasswordRequestForm
    from fastapi import Depends, APIRouter
    from . import security # 우리 모듈을 임포트합니다
    
    # ... 당신의 FastAPI 코드 ...
    router = APIRouter(prefix="/api/v1")
    
    @router.post("/login")
    def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
        # 여기에 DB 사용자 검증 로직이 있어야 합니다
        # 예를 들어, 하나의 테스트 사용자가 있습니다
        is_user_valid = (form_data.username == "testuser" and
                         security.verify_password("testpass", security.get_password_hash("testpass")))
    
        if not is_user_valid:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="잘못된 사용자 이름 또는 비밀번호",
            )
        access_token = security.create_access_token(data={"sub": form_data.username})
        return {"access_token": access_token, "token_type": "bearer"}
    
    # ...
    app.include_router(router)
    

  2. 엔드포인트 보호:

    우리 의존성 get_current_user를 사용합니다.

    # main.py 또는 당신의 행성 라우터에서
    
    @router.post("/planets", status_code=status.HTTP_201_CREATED)
    def create_planet(
        planet: PlanetCreate,
        current_user: dict = Depends(security.get_current_user) # <-- 보호!
    ):
        // 행성 생성 로직...
        print(f"사용자 {current_user['username']}이(가) 행성을 생성합니다.")
        // ...
        return new_planet
    
    // PUT  DELETE도 보호합니다
    


4. 프런트엔드에서 토큰 사용

이제 우리 프런트엔드는 먼저 토큰을 얻고, 그것을 저장하고(예: localStorage에), 각 보호된 요청에 첨부해야 합니다.

JavaScript (fetch) 예시:

// 1. 로그인
async function login(email, password) {
    const response = await fetch('http://localhost:8001/api/login', { // Laravel API 주소
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({email, password})
    });
    const data = await response.json();

    if (data.token) {
        localStorage.setItem('api_token', data.token); // 토큰 저장
    }
}

// 2. 보호된 요청 수행
async function createPlanet(planetData) {
    const token = localStorage.getItem('api_token');

    const response = await fetch('http://localhost:8001/api/planets', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}` // <--- 토큰 첨부!
        },
        body: JSON.stringify(planetData)
    });
    // ...
}


복습 퀴즈

1. Stateless API는 주로 인증에 다음을 사용합니다:

2. Laravel에서 토큰을 사용하여 라우트를 보호하는 데 사용되는 미들웨어는 다음과 같습니다:

3. FastAPI에서 로그인 폼 데이터를 가져오는 데 사용되는 종속성은 다음과 같습니다:

4. 보호된 요청에서 토큰은 클라이언트에서 서버로 어떻게 전달됩니까?


🚀 장 요약:

API에 "액세스 제어 시스템"을 구축했습니다. 이제 누구나 당신의 "은하계 데이터베이스"를 수정할 수 없습니다.

  • ✅ 토큰 기반 인증의 원리를 이해했습니다.
  • 🔐 Laravel Sanctum에서 토큰 발급 및 라우트 보호를 구현했습니다.
  • ⚙️ FastAPI에서 OAuth2 및 JWT 기반 인증을 설정했습니다.
  • 🛰️ 프론트엔드가 토큰을 저장하고 사용하는 방법을 배웠습니다.

당신의 API는 기능적일 뿐만 아니라 안전해졌습니다. 그러나 다른 개발자들이 이를 사용하려면 "사용 설명서"가 필요합니다.

📌 확인:

  • Postman 또는 Insomnia를 사용하여 토큰 없이 /api/planets (Laravel) 또는 /api/v1/planets (FastAPI)에 POST 요청을 시도해보십시오. 401 Unauthorized 오류를 받아야 합니다.
  • /login에 요청을 보내 토큰을 받고, 이를 Authorization 헤더에 추가한 다음 POST 요청을 다시 시도해보십시오. 요청이 성공적으로 완료되어야 합니다.