레거시 마이그레이션 프로젝트에서 스키마를 재설계하다 보면 인증 아키텍처를 반드시 건드리게 된다. “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가 필요한 순간은 생각보다 드물다.