HTTP 프로토콜의 Connectionless와 Stateless 특성 때문에, 사용자가 로그인해도 서버는 상태를 기억하지 않습니다. 즉, 로그인 이후 매 요청마다 다시 인증 정보를 보내지 않으면 인증된 상태를 유지할 수 없습니다.
하지만 실제 웹 애플리케이션에서는 로그인을 한 번 하면 이후 여러 페이지에서 반복 로그인을 하지 않아도 접속이 유지됩니다. 그 이유는 인증 정보를 클라이언트 혹은 서버에 저장하고 매 요청마다 인증 정보를 포함시키기 때문입니다. 그것이 어떻게 가능한지 살펴보겠습니다.
인증방식을 세션과 쿠키와 JWT로 나누어서 이해하기에는 다소 이해가 힘들어 인증정보 저장방식과 전달 정보로 나누어 글을 작성하였습니다.
먼저 인증정보 저장공간에는 클라이언트의 sessionStorage, localStorage, Cookie, Memory와 서버의 데이터베이스가 있습니다. 참고로 네이티브 모바일 앱 환경에서는 쿠키 지원이 제한적이거나 다르게 동작합니다.
인증 정보는 sessionStorage 에는 보통 인증 정보를 담지 않습니다. 브라우저 탭이 닫히면 sessionStorage는 날라가기 때문입니다. 이러한 이유로 sessionStorage는 제외하고 설명하겠습니다.
Memory도 브라우저 탭이 닫히면 날라가고 심지어 새로고침하더라도 날라갑니다. 그러나 빠른 속도를 위해서 Memory를 저장방식으로 종종 사용합니다. 그러나 위 글은 세션, 쿠키, JWT 방식의 차이점을 명확히 하기 위한 글로 Memory 저장방식은 제외하도록 하겠습니다.
전달 정보에는 세션ID, JWT, 사용자 명찰(사용자 식별 정보, 만료 정보)가 있습니다. ID/PW를 전달 정보에 포함하지 않는 이유는 가장 민감한 정보이기 때문에 직접적으로 사용되지 않는 것이 보안의 기본 원칙 중 하나이기 때문입니다.
1. localStorage, 사용자 명찰
localStorage를 사용하는 경우는 어떻게 해서 인증방식을 진행해야 할까요? 만약 각 사용자를 식별할 수 있는 ID/PW를 localStorage에 저장하고 매번 요청마다 사용자 명찰을 보낸다고 생각합시다. 이 요청은 XSS의 위험이 있습니다. 이 요청의 XSS 위험은 공격자가 악성스크립트를 심어 localStorage에 있는 사용자 명찰을 뺏는 행위를 말합니다. 또한 공격자가 사용자 명찰을 위조해서 보낼 수 있다는 취약점이 있습니다.
2. localStorage, JWT
1단계에서 문제가 된 위조 문제를 JWT 서명으로 완화할 수 있습니다. 그러나 여전히 XSS 취약점은 존재합니다.
(보안상 필요하다면 Refresh Token 도입 방식도 존재합니다. 밑에서 설명하겠습니다.)
3. 쿠키, 사용자 명찰(일명 쿠키)
localStorage + 사용자 명찰의 문제점 중 하나였던 XSS는 이 방식으로 해결할 수 있습니다.
JS가 쿠키에 접근하지 않도록 HttpOnly + Secure 설정하면 악성 스크립트로 사용자 명찰을 뺏을 수가 없습니다. 즉, XSS 방어가 가능합니다. 다만, 위조 문제는 해결하지 못했습니다. 그러나 저흰 위에서 위조 문제를 해결할 수 있는 방법을 알았습니다.
+) XSS는 막았지만 CSRF 문제가 발생합니다. 쿠키 인증 시 CSRF 방어는 필수적입니다.
- 사용자가 사이트에 로그인 상태여야 한다.
- 사용자는 조작된 페이지에 방문(접속)해야 한다
CSRF는 위와 같은 조건을 갖추면 발생할 수 있습니다. 쿠키는 요청마다 자동으로 인증정보를 주입해서 보내기 때문에 악성 사이트인 B 사이트에서 공격 요청을 보낸다면 자동으로 인증 정보가 담긴 쿠키가 담겨 본 서비스인 A사이트에게 요청이 성공적으로 전달됩니다.🥲
4. 쿠키, JWT
JWT를 사용하면 위조 문제를 해결할 수 있습니다. 즉, 쿠키로 XSS를 예방하고 JWT로 위조를 예방한 것입니다.
XSS 예방은 악성스크립로 삽입으로 인해 인증 정보 가져가는 것을 예방하는 것이라고 하였습니다. 그러나 다른 탈취 가능성도 존재합니다. CSRF, 중간자 공격(MITM: Man In The Middle)으로 토큰을 중간에 가로챌 수 있습니다.
이것을 방지하기 위해 두 가지 방식이 있습니다.
첫 번째 방법은 Access Token을 데이터베이스에 저장해서 블랙리스트 관리를 하는 것입니다. 허나 이 방식의 단점은 JWT의 장점인 Stateless 특성을 희석시킵니다. 매 요청마다 서버가 Access Token가 블랙리스트가 아닌지 확인해야 하는 과정이 포함되어 데이터베이스가 부담스럽습니다.
두 번째 방법은 Access Token의 유효기간은 짧게 잡는 것입니다. 표준적인 JWT는 한번 발급되면 서버가 해당 토큰의 유효 기간 전까지는 그 유효성을 스스로 폐기하기 어렵습니다. 그래서 ‘공격자에게 맞더라도 짧게 맞자!’ 라는 취지에서 유효 기간을 줄이는 것입니다. 그런데 유효기간을 짧게 잡게 되면 사용자가 새로 Access Token을 발급하기 위해 짧은 시간 단위로 계속 로그인을 해야 합니다. 불편하지 않겠습니까?
5. 쿠키, Access / Refresh JWT 분리
그래서 Access Token은 유효기간을 짧게 두고 유효기간이 긴 Refresh Token을 두는 것입니다. Refresh Token의 유효기간이 남아있다면 Access Token을 재발급해주는 방식입니다.
Refresh Token도 Access Token과 같이 쿠키에 있으니 탈취 가능성이 똑같은 거 아니냐? 라고 물을 수 있는데 맞습니다! 그러나 Refresh Token은 Access Token을 재발급 하는 요청에만 주고 받기 때문에 노출 빈도가 Access Token보다 현저히 적습니다. 이러한 이유로 Access Token보다 탈취 가능성이 적다고 말합니다.
(엑세스, 리프레쉬 토큰 저장방식은 refresh 토큰을 서버가 가지냐 클라이언트가 가지냐 아니면 둘 다 가지냐에 따라 달라지기 때문에 따로 그림은 그리지 않았습니다. Refresh Token 저장공간에 따른 분리는 다른 글에서 적도록 하겠습니다)
그런데 요구사항이 추가되었다고 가정해보겠습니다.
- 로그아웃시 엑세스 토큰, 리프레쉬 토큰 못사용하게 하기
- 혹은 컴퓨터1로 로그인했다가 컴퓨터2로 로그인하면 컴퓨터1은 로그아웃 하게 하기.
이를 해결하기 위해서는 Access Token 조차 데이터베이스에 저장하고 블랙리스트를 관리하는 방식으로 구현해도 됩니다. 그러면 앞서 말했다 시피 JWT의 Stateless 장점을 포기하는 것입니다. JWT는 길고 암호화된 정보를 포함하고 있기에 무겁고 JWT 검증 + 블랙리스트 체크 2 단계를 거쳐야 합니다.
이렇게 되면 똑같은 stateful인 세션방식을 사용하는 것이 나을 수도 있습니다.
6. localStorage + 데이터베이스, 세션(일명 세션방식)
그래서 세션방식을 도입했습니다. 세션방식은 데이터베이스에 세션정보를 담고 사용자가 세션ID를 보내면 데이터베이스에 그 세션이 존재하는지 확인하고 인증하는 방식입니다. 로그아웃과 만료를 하기위해서는 데이터베이스에서 세션정보를 삭제만 하면 되기 때문에 간편하다는 장점과 클라이언트의 상태를 직접 제어, JWT보다 가볍다는 측면에서는 쿠키 + JWT보다는 좋습니다.
그러나 지금껏 살펴보았던 1 ~ 4번 방식과의 차이점인 데이터베이스에 세션정보를 담고 관리해야 한다는 측면에서는 좋지 않습니다. 즉, 1 ~ 4번 방식보다 서버 데이터베이스의 부담이 있습니다. 그러기 때문에 해당 서비스에서 무엇을 중요시 하냐를 정하여 인증 방식을 선택해야 합니다.
자! 6번 방식을 보안측면에서 업그레이드 해보겠습니다.
7. 쿠키 + 세션(일명 세션쿠키방식)
앞서 살펴보았던 것처럼 localStorage에 정보를 담아두면 XSS 측면에서 위험이 있으니 쿠키에 저장하는 것으로 보안을 업그레이드 할 수 있습니다. (모바일 쿠키가 없습니다.)
위에서 더 좋은 방법으로 디벨롭하는 방법도 살펴보았지만
선택들 중 “모바일도 지원하는지?” “Stateless가 중요한지?”에 따라 다른 선택지가 있는지도 살펴보았습니다. 저의 결론은 아래와 같습니다.
로그아웃이 있는 서비스에서 JWT의 딜레마:
- 즉시 로그아웃을 구현하려면 → Access Token 블랙리스트 관리 필요
- 블랙리스트 관리 → 매 요청마다 DB/Redis 확인 → Stateless 포기
- 결국 세션 방식과 동일한 부하 + JWT 검증 오버헤드까지 추가
세션 쿠키 vs JWT 블랙리스트 비교:
- 세션 쿠키: 매 요청마다 세션 DB 확인 (1단계)
- JWT 블랙리스트: 매 요청마다 JWT 검증 + 블랙리스트 DB 확인 (2단계)
결론적으로 로그아웃 기능이 필요한 서비스라면 세션 쿠키가 더 합리적인 선택입니다.
JWT가 진짜 의미있는 경우:
- 로그아웃 기능이 있지만, 이전 엑세스 토큰을 무효화 시키지 않는 “약간의 보안 트레이드오프"를 감수하는 경우
- 완전 분산 환경에서 "약간의 보안 트레이드오프"를 감수하는 경우
상황 | 추천 방식 |
---|---|
단일 서버, 브라우저 기반, 단순 로그인만 필요한 경우 | 세션 쿠키 |
다양한 클라이언트, 리프레쉬 토큰 Stateful 감수한 API 서버 구조 | JWT + Refresh + Redis 관리 |
'CS > 개발지식' 카테고리의 다른 글
데이터베이스 시스템에서 동시성을 제어하는 방법? (0) | 2024.12.18 |
---|---|
동기 방식으로 외부 서비스를 호출할 때 외부 서비스 장애 조치 방법 (0) | 2024.12.11 |
실생활 예시로 이해하는 동기/비동기 & 블로킹/논블로킹 (1) | 2024.12.10 |