레거시 마이그레이션 프로젝트에서 스키마를 재설계하다 보면 인증 아키텍처를 반드시 건드리게 된다. “JWT 쓸까, Sanctum 쓸까, OAuth는 뭐지?” — 이 질문이 혼란스러운 이유는 세 개가 같은 레이어에 있지 않기 때문이다.
인증의 3가지 레이어
인증에는 3가지 독립적인 레이어가 있다:
| 레이어 | 질문 | 예시 |
|---|---|---|
| 프로토콜 | 누가 인증하는가? | OAuth, SAML, OpenID Connect |
| 토큰 포맷 | 토큰이 어떻게 생겼는가? | JWT(자체 검증), Opaque Token(DB 조회) |
| 전달 방식 | 토큰을 어떻게 보내는가? | Bearer 헤더, 쿠키, 쿼리스트링 |
이 3가지는 독립적이다. OAuth + JWT 조합도 가능하고, OAuth + Opaque Token도 가능하다. “JWT vs OAuth”라는 비교 자체가 레이어가 다른 걸 비교하는 것이다.
OAuth — “누가 인증하는가”의 프로토콜
카카오, 네이버, 구글 같은 제3자가 “이 사람 맞아”라고 확인해주는 프로토콜.
1. 사용자 → "카카오로 로그인" 클릭
2. 카카오 로그인 페이지로 리다이렉트
3. 사용자가 카카오에서 로그인
4. 카카오 → 우리 서버에 "이 사람은 user_id: 12345야" 전달
5. 우리 서버에서 자체 토큰 발행 (Sanctum 등)
핵심: OAuth는 로그인 시점에만 관여한다. 로그인 이후 API 통신은 우리 서버의 토큰 시스템(Sanctum 등)이 담당한다.
JWT (JSON Web Token) — 자체 검증 토큰
구조
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTcxMTM2MDAwMH0.서명값
──────헤더────────── ──────────페이로드────────────────────────────── ─서명─
3개 부분이 점(.)으로 연결된다. 각각 base64url 인코딩.
base64 인코딩이란
바이너리/텍스트를 영문자+숫자+기호만으로 표현하는 변환 방식이다.
원본: {"alg":"HS256"}
base64: eyJhbGciOiJIUzI1NiJ9
- 암호화가 아님 — 누구든 디코딩 가능
- HTTP 헤더에 안전하게 넣기 위한 인코딩
- JSON은 항상
{로 시작 → base64로 변환하면 항상ey로 시작 ey로 시작하는 문자열 = base64 인코딩된 JSON = 높은 확률로 JWT
발행 과정
1. 헤더 base64 인코딩: {"alg":"HS256"} → eyJhbGciOiJIUzI1NiJ9
2. 페이로드 base64 인코딩: {"user_id":123,"exp":1711360000} → eyJ1c2VyX2lk...
3. 헤더.페이로드를 서버 비밀키로 서명: HMAC-SHA256("헤더.페이로드", 비밀키) → 서명값
4. 세 부분 연결: 헤더.페이로드.서명
검증 과정 (DB 조회 없음)
1. 받은 토큰에서 헤더.페이로드 분리
2. 서버 비밀키로 서명 재생성
3. 재생성한 서명 == 토큰의 서명? → 위조 안 됨
4. 페이로드의 exp 확인 → 만료 안 됨?
5. 통과 → user_id: 123 신뢰
해커가 페이로드를 변조하면 서명이 안 맞아서 서버가 거부한다. 페이로드는 누구나 볼 수 있지만, 서명은 비밀키 없이 만들 수 없다.
장단점
- 장점: DB 조회 0회. 비밀키만 있으면 어떤 서버든 검증 가능. 마이크로서비스에 유리.
- 단점: 즉시 폐기 불가 (토큰 만료까지 유효). 기기별 토큰 관리 어려움. 토큰 탈취 시 만료까지 대응 불가.
Opaque Token (Sanctum) — DB 기반 토큰
구조
1|abc123def456...
─ ───────────────
ID 랜덤 문자열
토큰 자체에 정보가 없다. DB 조회해야 의미를 알 수 있다.
발행 과정
1. 랜덤 문자열 생성
2. SHA-256 해시 → DB에 저장 (personal_access_tokens 테이블)
3. 원본 문자열 → 앱에 전달
검증 과정 (DB 조회 1회)
1. 앱이 보낸 토큰 수신
2. SHA-256 해시
3. personal_access_tokens 테이블에서 해시값 매칭
4. 매칭되면 → 해당 사용자 확인
장단점
- 장점: 즉시 폐기 가능 (DB에서 삭제). 기기별 토큰 관리 용이. 구현 단순.
- 단점: 매 요청마다 DB 조회 1회 (PK 인덱스라 매우 빠름). 분산 시스템에서 DB 공유 필요.
JWT vs Opaque Token 비교
| JWT | Opaque Token (Sanctum) | |
|---|---|---|
| DB 조회 | 없음 | 요청마다 1회 |
| 즉시 폐기 | ❌ | ✅ |
| 기기별 관리 | 복잡 | 쉬움 |
| 토큰 탈취 대응 | 만료까지 대기 | 즉시 삭제 |
| 분산 시스템 | 유리 | DB 공유 필요 |
| 구현 복잡도 | 높음 (키 관리, 갱신) | 낮음 |
Laravel이 Sanctum(Opaque Token)을 선택한 이유: 대부분의 프로젝트에서 JWT의 장점(DB 미조회)보다 Opaque Token의 장점(즉시 제어)이 더 중요하다. DB 조회 1회의 비용은 극히 작고(PK 인덱스), 대부분의 웹 서비스에서 병목이 되지 않는다.
Laravel Sanctum 실제 흐름
토큰 발행 (로그인 시)
$token = $user->createToken('app-kakao', ['*'], $expiresAt)->plainTextToken;
// → "1|abc123def456..."
API 요청 (앱에서)
GET /v1/car/list
Authorization: Bearer 1|abc123def456...
서버 검증 (자동)
auth:sanctum 미들웨어가 처리:
1) 토큰 ID 추출 (1)
2) personal_access_tokens 테이블 조회
3) 해시값 비교
4) 만료 확인
5) $request->user()에 사용자 바인딩
서버투서버(S2S) 인증 — Sanctum과 별개
Sanctum은 사용자 인증 전용이다. 서버 간 통신은 별도 체계가 필요하다:
| 방식 | 설명 | 용도 |
|---|---|---|
| API Key | 공유 비밀키를 헤더에 포함 | 내부 서비스 간 |
| HMAC 서명 | payload를 비밀키로 서명 | 웹훅, 결제 콜백 |
| JWT | 서비스 인증서로 토큰 발행 | 마이크로서비스 |
단일 서버 아키텍처에서는 S2S 인증이 불필요하다. 관리자 서버 분리 시 필요해질 수 있다.
Laravel 퍼스트 파티 인증 패키지
| 패키지 | 방식 | 용도 |
|---|---|---|
| Sanctum | Opaque Token | API 인증 (모바일앱, SPA) |
| Passport | OAuth2 서버 (JWT 내부 사용) | 서드파티에 API 제공 시 |
| Fortify | 인증 백엔드 로직 | 로그인/회원가입/2FA 등 |
| Breeze/Jetstream | 인증 UI + 로직 | 풀 인증 스캐폴딩 |
순수 JWT 패키지는 공식에 없다 (tymon/jwt-auth 서드파티만 존재).
정리
“JWT 쓸까 Sanctum 쓸까”를 고민하기 전에, 지금 내 프로젝트에서 뭐가 중요한지 먼저 본다:
- 마이크로서비스 없이 단일 서버? → Sanctum (대부분 여기)
- 토큰 즉시 폐기가 중요? → Sanctum
- 서비스 간 인증 DB 공유가 불가능? → JWT
- 서드파티에 API를 열어야 함? → Passport
대부분의 Laravel 프로젝트에서 정답은 Sanctum이다. JWT가 필요한 순간은 생각보다 드물다.