레거시 마이그레이션 프로젝트에서 스키마를 재설계하다 보면 인증 아키텍처를 반드시 건드리게 된다. “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 비교

JWTOpaque 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 퍼스트 파티 인증 패키지

패키지방식용도
SanctumOpaque TokenAPI 인증 (모바일앱, SPA)
PassportOAuth2 서버 (JWT 내부 사용)서드파티에 API 제공 시
Fortify인증 백엔드 로직로그인/회원가입/2FA 등
Breeze/Jetstream인증 UI + 로직풀 인증 스캐폴딩

순수 JWT 패키지는 공식에 없다 (tymon/jwt-auth 서드파티만 존재).


정리

“JWT 쓸까 Sanctum 쓸까”를 고민하기 전에, 지금 내 프로젝트에서 뭐가 중요한지 먼저 본다:

  • 마이크로서비스 없이 단일 서버? → Sanctum (대부분 여기)
  • 토큰 즉시 폐기가 중요? → Sanctum
  • 서비스 간 인증 DB 공유가 불가능? → JWT
  • 서드파티에 API를 열어야 함? → Passport

대부분의 Laravel 프로젝트에서 정답은 Sanctum이다. JWT가 필요한 순간은 생각보다 드물다.