이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › 보안 › 인증
세션과 JWT
로그인 상태는 어디에 저장되나
로그인한 사용자가 10분 간격으로 요청을 보내고, 로드밸런서가 요청을 서버에 돌아가며 나눠 줍니다. 로그인 상태를 서버가 기억하는 방식(세션)과 클라이언트가 들고 다니는 방식(JWT)에서, 서버를 늘리거나 강제 로그아웃을 시킬 때 무슨 일이 생기는지 보세요.
개념 모형 · 널리 쓰이는 구성을 옮긴 모형
이 실습에 나오는 개념
- 쿠키
- 서버가 응답에 실어 보내면 브라우저가 저장해 두었다가 같은 사이트에 요청할 때마다 자동으로 함께 보내는 작은 데이터다. HTTP는 요청 사이의 상태를 기억하지 않으므로, 로그인 상태처럼 이어져야 하는 정보를 유지하는 데 쓰인다. 사용자의 브라우저에 저장되므로 내용을 보거나 바꿀 수 있다는 점을 전제로 사용해야 한다.
- 세션
- 사용자의 상태를 서버 쪽에 저장하고, 클라이언트에는 그 저장소를 가리키는 식별자(세션 ID)만 쿠키로 전달하는 방식이다. 실제 정보가 서버에 있으므로 클라이언트가 내용을 고칠 수 없다(세션 ID를 빼앗기는 것은 다른 문제다). 서버가 여러 대일 때는 어느 서버로 요청이 가도 같은 세션을 찾을 수 있도록 Redis 같은 공용 저장소에 세션을 두는 구성이 일반적이다.
- JWT(JSON Web Token)
- 사용자 정보 등의 내용(클레임)을 담고 서명을 붙인 토큰이다. 헤더, 페이로드, 서명의 세 부분으로 이뤄진다. 서버는 서명을 검증해 토큰이 위조되지 않았음을 확인하며, 별도의 저장소를 조회하지 않아도 된다. 페이로드는 암호화된 것이 아니라 인코딩된 것이어서 누구나 내용을 볼 수 있으므로 민감한 정보를 넣지 않는다.
- 액세스 토큰과 리프레시 토큰
- 액세스 토큰은 API를 호출할 때 제시하는 토큰으로, 탈취되었을 때의 피해를 줄이기 위해 유효 기간을 짧게 둔다. 리프레시 토큰은 액세스 토큰이 만료되었을 때 새 액세스 토큰을 발급받는 데 쓰는 토큰으로, 유효 기간이 길고 더 안전하게 보관한다. 서버는 리프레시 토큰을 폐기해 재발급을 막을 수 있다.
- HTTP와 무상태(Stateless)
- HTTP는 클라이언트가 요청을 보내면 서버가 응답하는 방식의 통신 규약이다. 무상태라는 것은 서버가 이전 요청의 내용을 기억하지 않아 각 요청이 그 자체로 처리에 필요한 정보를 모두 담고 있어야 한다는 뜻이다. 서버가 상태를 기억하지 않으므로 어느 서버가 요청을 받아도 되어 서버를 늘리기 쉽다. 로그인 상태처럼 이어져야 하는 정보는 쿠키, 세션, 토큰으로 유지한다.
이렇게 바꿔 보세요
- 처음 설정으로 — 서버 메모리 세션, 서버 2대. 다른 서버로 간 요청은 로그인이 풀립니다.
- 세션을 Redis로 옮기면 — 어느 서버로 가도 로그인이 유지됩니다. 대신 요청마다 저장소를 조회합니다.
- JWT로 바꾸면 — 서버는 아무것도 기억하지 않습니다. 25분 뒤 토큰이 만료됩니다.
- JWT인데 강제 로그아웃 — 이미 발급한 토큰은 만료 전까지 막을 방법이 없습니다.
- 세션인데 강제 로그아웃 — 저장소에서 세션을 지우면 바로 다음 요청부터 막힙니다.
- 짧은 액세스 토큰 + 리프레시 토큰 — 15분마다 재발급합니다. 강제 로그아웃이 늦어도 15분 안에 먹힙니다.
처음 설정으로 실행하면
- 0분: 서버 1에서 로그인했습니다. 서버 1이 자기 메모리에 세션을 만들고, 세션 ID를 쿠키로 내려 줍니다. 서버 1만 이 사용자를 기억합니다.
- 10분: 요청이 서버 2로 갔습니다. 쿠키로 세션 ID를 보냈지만 이 서버 메모리에는 그런 세션이 없어서 로그인하지 않은 사용자로 취급합니다.
- 20분: 요청이 서버 1로 갔습니다. 이 서버 메모리에 세션 abc123이 있어서 로그인 상태로 처리합니다.
- 30분: 요청이 서버 2로 갔습니다. 쿠키로 세션 ID를 보냈지만 이 서버 메모리에는 그런 세션이 없어서 로그인하지 않은 사용자로 취급합니다.
- 40분: 요청이 서버 1로 갔습니다. 이 서버 메모리에 세션 abc123이 있어서 로그인 상태로 처리합니다.
- 50분: 요청이 서버 2로 갔습니다. 쿠키로 세션 ID를 보냈지만 이 서버 메모리에는 그런 세션이 없어서 로그인하지 않은 사용자로 취급합니다.
실행 결과
- 세션이 로그인한 서버 한 대의 메모리에만 있어서, 다른 서버로 간 요청은 로그인이 풀렸습니다. 서버를 여러 대로 늘리려면 같은 사용자를 항상 같은 서버로 보내거나(스티키 세션), 세션을 공용 저장소로 옮겨야 합니다.
코드
// 로그인 성공 시
HttpSession session = request.getSession();
session.setAttribute("memberId", member.getId());
// 응답 헤더: Set-Cookie: JSESSIONID=abc123; HttpOnly
// 이후 요청마다: 쿠키의 세션 ID로 세션을 찾는다
HttpSession session = request.getSession(false);
if (session == null) {
throw new UnauthorizedException(); // 401
}
이 모형의 전제
로드밸런서는 요청을 서버에 차례로 돌아가며 보낸다고 가정했습니다(로그인은 서버 1, 그다음 요청은 서버 2 …). 세션 만료 시간과 리프레시 토큰 만료(보통 며칠에서 몇 주)는 이 50분 안에 닿지 않는다고 보았습니다. JWT의 서명 검증은 저장소 없이 서버가 가진 키만으로 이루어집니다. 리프레시 토큰을 서버 저장소에 두고 확인하는 것은 널리 쓰이는 구성이지만 표준이 강제하는 것은 아닙니다.
다음 실습: 쿠키