🔑 인증, 고를 게 많다
백엔드 스터디를 하면서 드디어 인증을 구현하게 되었다. 강의에서는 가볍게 JWT 토큰을 헤더에 저장하는 방식만 설명하고 있어 가볍게 많이 사용하는 방식들에 대해 더 공부해보고자 한다.
인증에는 네 가지 선택지가 있다
인증을 공부하다 보면 "JWT vs 세션", "쿠키 vs 헤더" 같은 비교를 자주 마주친다.
그런데 이것들은 서로 다른 층위의 이야기다.
| 선택지 | 질문 | 예시 |
|---|---|---|
| 인증 수단 | 뭘로 신원을 증명하나? | 세션ID, JWT |
| 전달 방식 | 클라이언트와 서버가 어떤 경로로 주고받나? | 쿠키, 헤더 |
| 서버 저장소 | 서버는 어디에 기억해두나? | 없음, Redis, DB |
| 클라이언트 저장소 | 클라이언트는 어디에 보관하나? | 쿠키, 로컬스토리지, Secure Store |
이 네 선택지는 독립적이다. 그래서 조합이 가능하다.
예를 들어 같은 JWT라도, 웹에서는 쿠키에 담아 보내고 모바일에서는 헤더에 붙여 보낼 수 있다. 인증 수단이 같아도 전달 방식은 다를 수 있다.
인증 수단 — "뭘로 증명하나?"
인증이란 서버에게 "나 이 사람이야"를 증명하는 방식이다.
세션 (Session)
서버가 상태를 기억한다. 세션ID는 열쇠일 뿐이고, 실제 정보는 서버 저장소에 있다.
JWT (JSON Web Token)
세션과 반대로 서버가 아무것도 기억하지 않는다(Stateless)
토큰 자체에 유저 정보(ID, 역할 등)가 들어있고, 서버는 "이 토큰이 내가 서명한 게 맞는지"만 확인한다.
여기에 함정이 있다. 서버가 아무것도 기억하지 않으니, 토큰을 서버에서 무효화할 수 없다. 유저가 로그아웃해도, 토큰이 탈취돼도, 만료 시간이 될 때까지 토큰은 유효하다.
JWT + Refresh Token
순수 JWT의 한계를 보완한 방식이다.
Access Token 수명을 짧게 가져가 탈취 위험을 줄이고, Refresh Token은 서버가 관리해 강제 무효화를 가능하게 한다. Stateless의 장점은 살리되, 필요한 부분만 서버가 관리하는 하이브리드 방식이다.
비교
| 세션 | JWT | JWT + Refresh | |
|---|---|---|---|
| 서버 상태 | Stateful | Stateless | 하이브리드 |
| 강제 로그아웃 | 쉬움 | 어려움 | 가능 |
| 서버 부담 | 매 요청 저장소 조회 | 서명 검증만 | 서명 검증 + 일부 조회 |
| 토큰 탈취 시 | 세션 삭제로 차단 | 만료까지 유효 | 짧은 수명으로 완화 |
전달 방식 — "어떻게 보내나?"
인증 수단을 정했으면, 클라이언트와 서버가 어떤 경로로 주고받을지 정해야 한다.
쿠키 (Cookie)
서버가 응답에 Set-Cookie를 보내면, 브라우저가 이후 매 요청에 쿠키를 자동으로 포함시킨다. 프론트 코드에서 별도로 붙이는 작업이 필요 없다.
가장 큰 장점은 HttpOnly 옵션이다. JavaScript에서 쿠키에 접근할 수 없어, 악성 스크립트가 토큰을 훔치는 XSS 공격을 원천 차단한다.
Secure과 SameSite옵션으로도 중간자 공격이나 csrf공격을 방어할 수 있다.
단점은 브라우저 환경에서만 의미가 있다는 것이다. 모바일 네이티브 앱에는 브라우저가 없으니 자동 전송이나 HttpOnly 보호가 작동하지 않는다.
Authorization 헤더 (Bearer Token)
요청을 보낼 때 코드로 직접 헤더에 토큰을 붙인다.
브라우저 의존이 없어서 모바일 앱, SPA, 서버 간 통신 어디서든 쓸 수 있다. 대신 토큰을 저장하고 꺼내서 붙이는 건 개발자 몫이다. 프론트 입장에서 토큰 관리 방식이 중요해지는 이유다.
비교
| 쿠키 | Authorization 헤더 | |
|---|---|---|
| 전송 | 브라우저가 자동 | 코드로 직접 |
| XSS 방어 | HttpOnly로 JS 접근 차단 | 저장소에 따라 다름 |
| CSRF 위험 | 있음 (SameSite로 완화) | 없음 |
| 모바일 | 불편함 | 적합 |
| 웹 | 적합 | 가능 |
서버 저장소 — "서버는 어디에 기억하나?"
순수 JWT처럼 서버가 아무것도 저장하지 않는 경우도 있지만, 대부분의 서비스에서는 서버 쪽에도 뭔가를 저장한다. 세션 정보일 수도 있고, 토큰 유효 여부일 수도 있다.
없음 (순수 Stateless)
서버가 아무것도 저장하지 않는다. JWT 서명만 검증하면 끝이다.
서버 확장이 쉽다. 서버가 10대든 100대든 같은 비밀키만 공유하면 어느 서버에서든 토큰을 검증할 수 있다. 대신 강제 무효화가 안 된다.
메모리 (서버 변수)
서버 프로세스 메모리에 세션을 저장한다. 가장 빠르지만, 서버가 여러 대면 문제가 된다. 유저가 A 서버에서 로그인했는데 다음 요청이 B 서버로 가면 세션을 찾을 수 없다.
Redis
인메모리 저장소로, DB보다 빠르고 서버 간 공유가 가능하다.
서버가 여러 대여도 모두 같은 Redis를 바라보니 세션 공유 문제가 해결된다. 활용 예시도 다양하다.
| 용도 | 저장하는 것 | 예시 |
|---|---|---|
| 세션 저장소 | 세션ID → 유저 정보 | 전통 세션 방식 |
| 토큰 블랙리스트 | 로그아웃된 토큰 ID(jti) | JWT 강제 무효화 |
| 세션 유효성 | 세션ID → 유효 여부 (Y/N) | JWT + 세션 하이브리드 |
| Refresh Token | Refresh Token 해시 | 토큰 갱신 관리 |
DB (PostgreSQL, MySQL 등)
영구 저장이 필요할 때 사용한다. Redis보다 느리지만 데이터가 날아가지 않는다. 실시간 조회는 Redis에, 보안 로그나 세션 이력은 DB에 두는 식으로 함께 쓰는 경우가 많다.
비교
| 없음 | 메모리 | Redis | DB | |
|---|---|---|---|---|
| 속도 | — | 가장 빠름 | 빠름 | 상대적으로 느림 |
| 서버 확장 | 자유로움 | 불가 | 가능 | 가능 |
| 영구성 | — | 서버 재시작 시 소멸 | 설정에 따라 | 영구 |
| 강제 무효화 | 불가 | 가능 | 가능 | 가능 |
| 주 용도 | 순수 JWT | 단일 서버 개발 | 세션/토큰 관리 | 이력/영구 저장 |
클라이언트 저장소 — "클라이언트는 어디에 보관하나?"
서버에서 받은 토큰이나 세션ID를 클라이언트 어디에 저장할지의 문제다. 이 선택이 보안에 직접적인 영향을 미친다.
쿠키
서버가 Set-Cookie로 설정하면 브라우저가 관리한다. HttpOnly를 켜면 JavaScript에서 접근할 수 없다.
로컬스토리지 (localStorage)
브라우저를 닫아도 유지된다. 편하지만 JavaScript로 자유롭게 접근 가능해, XSS 공격에 취약하다.
세션스토리지 (sessionStorage)
로컬스토리지와 비슷하지만 탭을 닫으면 사라진다. XSS 취약점은 동일하다.
Secure Store (모바일)
모바일 OS가 제공하는 암호화된 저장소다. iOS의 Keychain, Android의 Keystore가 이에 해당한다. React Native에서는 expo-secure-store 같은 라이브러리를 통해 접근한다. 앱 외부에서는 접근이 불가능하다.
메모리 (변수)
새로고침하면 사라진다. 가장 안전하지만 가장 불편하다. 수명이 짧고 Refresh Token으로 재발급 가능한 Access Token에 쓰기도 한다.
비교
| 쿠키 (HttpOnly) | 로컬스토리지 | 세션스토리지 | Secure Store | 메모리 | |
|---|---|---|---|---|---|
| JS 접근 | 불가 | 가능 | 가능 | 불가 (앱 외부) | 가능 (앱 내부만) |
| XSS 위험 | 안전 | 취약 | 취약 | 해당 없음 | 안전 |
| 지속성 | 만료까지 | 영구 | 탭 닫으면 소멸 | 영구 | 새로고침 시 소멸 |
| 환경 | 웹 | 웹 | 웹 | 모바일 | 어디든 |
보안 위협이 선택을 결정한다
환경마다 다른 저장소를 쓰는 이유는 위협의 종류가 다르기 때문이다.
웹의 주요 위협은 XSS다. 악성 스크립트가 페이지에 삽입되면 JavaScript로 접근 가능한 모든 저장소의 토큰을 탈취할 수 있다. JS가 접근할 수 없는 HttpOnly 쿠키가 안전한 이유다.
모바일의 주요 위협은 기기 탈취와 리버스 엔지니어링이다. 브라우저가 없으니 XSS는 해당이 없고, OS 레벨에서 암호화해주는 Secure Store가 가장 안전하다. HttpOnly가 JS 접근을 막는 보호라면, Secure Store는 앱 외부의 접근을 막는 보호인 셈이다.
| 환경 | 주요 위협 | 추천 저장소 | 추천 전달 방식 |
|---|---|---|---|
| 웹 | XSS | HttpOnly 쿠키 | 쿠키 (자동 전송) |
| 모바일 앱 | 기기 탈취 | Secure Store | 헤더 (직접 전송) |
| 웹 + 모바일 | 둘 다 | 환경별 분기 | 쿠키 + 헤더 둘 다 지원 |
마치며
그동안 서버가 이 토큰을 어떻게 검증하는지, 세션을 어디에 저장하는지, 왜 Redis를 쓰는지 같은 건 생각해본 적이 없었다.
백엔드 스터디를 하면서 서버는 단순히 토큰을 발급하고 검증하는 것에서 끝이 아니라는 걸 알게 되었고, 글을 쓴 것 이외에도 이미 폐기된 Refresh Token이 재사용되면 탈취로 판단해 전체 세션을 강제 로그아웃시키는 재사용 감지(Reuse Detection), 세션 유효성을 Redis에서 빠르게 확인하는 캐싱 전략 등 보안을 위해 다양한 방식으로 토큰을 관리하는 방식등 다양한 것을 공부해보면 좋을 것 같다는 생각이 든다 !...