이전 포스팅에서 안전한 로그인 흐름을 구현해 보았다. 그렇다면 성공적으로 로그인한 사용자의 '상태'는 어떻게 유지할 수 있을까? 웹 생태계에서 이를 해결하는 대표적인 두 가지 정답은 세션(Session) 기반 인증과 토큰(Token, JWT) 기반 인증이다.
세션 기반 인증은 사용자가 로그인에 성공하면 서버가 '세션 ID'를 생성하여 중앙 세션 저장소(메모리 또는 DB)에 기록하고, 이를 클라이언트에게 전달하는 방식이다. 이후 클라이언트는 요청마다 세션 ID를 보내고, 서버는 저장소를 뒤져 유저를 확인한다.
이 방식은 서버가 로그인 상태를 직접 기억하고 통제하는 상태 유지(Stateful) 방식이다. 보안 통제(강제 로그아웃 등)가 쉽다는 장점이 있지만, 치명적인 단점이 있다. 사용자가 늘어나 트래픽이 몰릴 때 서버를 여러 대로 확장(Scale-out)하게 되면, 모든 서버가 세션 저장소를 공유하고 매번 조회해야 하므로 성능 병목이 발생하기 쉽다.
그렇다면 토큰 기반 인증은 어떨까? 흔히 오해하지만, JWT(JSON Web Token)는 그 자체로 인증 방식이라기보다는 정보를 안전하게 '전달'하는 웹 표준 규격이다.
동작 흐름
1. 클라이언트가 이메일 + 비밀번호로 로그인 요청을 보낸다.
2. 서버는 DB에서 사용자를 조회하고 해시된 비밀번호를 검증한다.
3. 검증이 완료되면, 서버는 유저 데이터와 서버만의 '비밀키(Secret)'를 조합해 서명된 JWT를 발급하여 돌려준다.
4. 이후 클라이언트는 API 요청 시 헤더(또는 쿠키)에 토큰을 담아 보낸다.
5. 서버는 토큰 안의 페이로드와 자신의 비밀키를 이용해 '서명'을 다시 만들어보고, 클라이언트가 보낸 서명과 일치하는지 검증만 수행한다.
이 방식은 서버가 로그인 상태를 메모리에 따로 기억할 필요 없이, 들어온 토큰의 위조 여부만 수학적으로 검사하면 되는 무상태(Stateless) 방식이다. 따라서 서버 확장이 매우 자유롭고, 로드밸런싱이나 마이크로서비스 아키텍처(MSA)에 완벽하게 부합한다.
JWT는 .을 기준으로 세 구획으로 나뉜다.
여기서 가장 주의할 점은 페이로드의 데이터는 누구나 Base64로 디코딩해서 볼 수 있다는 것이다. 서명이 있기 때문에 데이터가 중간에 '변조'되었는지는 알 수 있지만, 암호화되어 가려진 것은 아니다. 따라서 페이로드에는 비밀번호나 개인정보 같은 민감한 데이터를 절대 넣어서는 안 된다.
토큰의 페이로드에는 반드시 '만료 시간(Expiration)'이 포함된다. 만료 시간이 너무 길면 해커에게 토큰이 탈취되었을 때 피해가 걷잡을 수 없이 커진다. 반대로 너무 짧으면 사용자가 수시로 재로그인을 해야 하는 끔찍한 UX를 겪게 된다.
이를 해결하기 위해 실무에서는 두 가지 토큰을 함께 사용하는 투트랙 전략을 취한다.
완벽해 보이는 JWT도 뼈아픈 대가를 치러야 한다. 만약 해커가 수명이 짧은 Access Token을 탈취했거나, 악성 유저를 즉각적으로 차단해야 하는 상황이 오더라도 서버는 토큰이 만료될 때까지 이를 중앙에서 제재할 방법이 없다. 서버가 상태를 기억하지 않기 때문이다.
그래서 실무에서는 Access Token은 Stateless하게 검증하되, 수명이 긴 Refresh Token은 서버의 DB나 인메모리 저장소에 기록하여 Stateful하게 관리하는 방식을 사용한다. 사용자가 로그아웃하거나 계정이 정지되면, 서버의 저장소에서 해당 Refresh Token을 지워버려 새로운 Access Token 발급을 원천 차단하는 원리다.
더 나아가, 핵심 API(결제, 비밀번호 변경 등)에서는 토큰 서명만 믿지 않고 하이브리드 방식으로 실시간 DB 조회를 거치기도 한다. 이때 발생하는 DB 병목 현상은 보통 인메모리 캐시인 Redis를 활용해 속도 문제를 극복한다.
발급받은 이 두 토큰을 클라이언트 어디에 보관할 것인가는 보안의 핵심이다.
SameSite 속성 등 설정으로 상당 부분 방어가 가능하다.)💡 결론적으로 실무에서는:
수명이 짧은 Access Token은 클라이언트의 로컬 메모리(JS 전역 변수 등)에 저장하여 API 요청 시 헤더에 직접 심어(Authorization: Bearer) 보내고, 수명이 길고 중요한 Refresh Token은 HttpOnly, Secure 속성이 걸린 쿠키에 저장하여 해커의 접근을 원천 차단하는 방식을 표준으로 삼는다.
Refresh Token을 쿠키에 안전하게 숨겼더라도, 네트워크 스니핑 등으로 탈취당할 가능성은 여전히 존재한다. 해커가 리프레시 토큰을 얻으면 엑세스 토큰을 무한정 재발급받을 수 있는 치명적인 문제가 생긴다.
이를 방어하기 위해 Token Rotation (RTR) 기법을 도입한다. 이 방식은 사용자가 Refresh Token을 사용해 새 Access Token을 발급받을 때, Refresh Token도 무조건 새것으로 교체(1회용으로 사용)하는 방식이다. 만약 해커가 이미 사용되어 폐기된 구형 Refresh Token을 들고 서버에 나타난다면, 서버는 즉시 이를 토큰 탈취 상황으로 간주하고 해당 유저의 모든 토큰 장부를 삭제시켜 완전히 강제 로그아웃 처리해 버린다.