우리는 웹사이트에 로그인하기 위해 아이디와 비밀번호를 입력한다.
그런데 생각해 보면 이상한 점이 하나 있다.
처음 로그인할 때는 분명 비밀번호를 입력했지만, 이후 게시글을 작성하거나 마이페이지에 접속할 때는 비밀번호를 다시 입력하지 않는다.
심지어 브라우저를 새로고침해도 로그인 상태가 유지된다.
서버는 어떻게 내가 이미 로그인한 사용자라는 사실을 기억하는 걸까?
여기서 조금 더 흥미로운 질문을 던져보자.
만약 내가 사용하는 로그인 정보를 다른 사람이 가져간다면, 비밀번호를 몰라도 내 계정에 접근할 수 있을까?
상황에 따라 가능하다.
이 문제를 이해하려면 HTTP의 특성과 쿠키, 세션, JWT의 동작 원리를 살펴봐야 한다.
1. HTTP는 사용자를 기억하지 않는다
HTTP는 기본적으로 Stateless, 즉 무상태 프로토콜이다.
서버는 각각의 HTTP 요청을 독립적으로 처리한다.
다음과 같은 상황을 생각해 보자.
사용자 서버
| |
| POST /login |
| -------------------> |
| |
| 로그인 성공 |
| <------------------- |
| |
| GET /mypage |
| -------------------> |
| |
| "당신이 누구죠?" |
HTTP 자체만 놓고 본다면 서버가 앞서 로그인에 성공한 사용자와 지금 마이페이지에 접근한 사용자가 같은 사람이라는 사실을 자동으로 알 수는 없다.
그렇다면 매번 비밀번호를 전송해야 할까?
그럴 필요는 없다.
로그인에 성공했다는 사실을 다른 방법으로 증명하면 된다.
여기서 세션(Session)과 쿠키(Cookie)가 등장한다.
2. 쿠키와 세션은 무엇이 다를까?
먼저 쿠키는 브라우저가 저장하고 HTTP 요청에 포함해 전송할 수 있는 작은 데이터다.
세션은 사용자의 상태를 유지하기 위한 개념이며, 전통적인 서버 세션 방식에서는 서버가 로그인 상태를 저장하고 세션 식별자를 통해 해당 상태를 조회한다.
예를 들어 사용자가 로그인에 성공하면 서버는 세션을 생성할 수 있다.
[서버의 세션 저장소]
session_id: abc123...
user_id: 101
login_status: true
이후 서버는 브라우저에 세션 식별자를 쿠키로 전달한다.
Set-Cookie: SESSION_ID=abc123...;
Path=/;
HttpOnly;
Secure;
SameSite=Lax
브라우저는 조건에 부합하는 후속 요청에 해당 쿠키를 포함한다.
GET /mypage HTTP/1.1
Host: example.com
Cookie: SESSION_ID=abc123...
서버는 세션 식별자를 이용해 세션 저장소를 조회한다.
Client Server
| |
| ------- 로그인 요청 -------> |
| |
| <---- 세션 쿠키 발급 ------- |
| |
| ------ 마이페이지 요청 ----> |
| SESSION_ID 포함 |
| |
| 세션 조회 |
| |
| <------ 사용자 정보 -------- |
이제 사용자는 매번 비밀번호를 입력하지 않아도 된다.
그런데 여기서 중요한 사실이 있다.
세션 식별자가 유효한 로그인 상태를 증명하는 수단으로 사용된다는 점이다.
서버가 세션 식별자의 소유 여부만으로 사용자를 인증한다면, 유효한 세션 식별자를 탈취한 공격자는 비밀번호를 몰라도 해당 사용자처럼 행동할 수 있다.
이것이 세션 하이재킹(Session Hijacking)의 핵심 원리다.
3. 세션 쿠키를 보호해야 하는 이유
세션 식별자는 단순한 문자열처럼 보이지만, 사실상 로그인 상태를 증명하는 중요한 자격 증명이다.
따라서 일반적인 데이터보다 신중하게 관리해야 한다.
대표적인 쿠키 보안 속성을 살펴보자.
| 속성 | 역할 |
|---|---|
| HttpOnly | JavaScript의 document.cookie로 접근하지 못하도록 제한 |
| Secure | HTTPS 연결에서만 쿠키를 전송 |
| SameSite | 다른 사이트에서 시작한 요청에 쿠키를 포함하는 방식을 제한 |
| Max-Age | 쿠키의 유효기간 설정 |
여기서 흔히 오해하는 부분이 있다.
HttpOnly를 설정하면 XSS 공격이 완전히 방지되는 것일까?
그렇지는 않다.
HttpOnly는 JavaScript에서 쿠키 값을 직접 읽는 것을 제한하지만, 악성 스크립트가 사용자의 브라우저 안에서 인증된 요청을 보내는 행위까지 모두 차단하지는 못한다.
마찬가지로 SameSite는 CSRF 방어에 도움을 주지만, 모든 환경에서 별도의 CSRF 방어 전략을 대체할 수 있는 것은 아니다.
보안 기능은 각각 해결하는 문제가 다르다.
그렇다면 최근 많이 사용하는 JWT는 어떻게 다를까?
4. JWT는 암호화된 비밀문서가 아니다
JWT(JSON Web Token)는 인증과 인가 정보를 전달하는 데 널리 사용하는 토큰 형식이다.
가장 익숙한 형태는 다음과 같다.
HEADER.PAYLOAD.SIGNATURE
일반적인 서명된 JWT는 각 부분이 Base64url 방식으로 인코딩되어 있다.
예를 들어 Payload에 다음과 같은 내용이 들어 있다고 가정하자.
{
"sub": "101",
"role": "USER",
"exp": 1893456000
}
여기서 sub는 주체를 식별하는 값이고, exp는 만료 시간을 의미하는 클레임이다.
중요한 사실은 Base64url 인코딩은 암호화가 아니라는 점이다.
일반적인 서명형 JWT의 Payload는 토큰을 가진 사람이 디코딩하여 내용을 확인할 수 있다.
따라서 비밀번호와 같은 민감한 정보를 그대로 넣어서는 안 된다.
그렇다면 Signature는 무슨 역할을 할까?
서명은 토큰이 신뢰할 수 있는 발급자에 의해 만들어졌는지, 내용이 변조되지 않았는지 검증하는 데 사용된다.
단, 알고리즘과 키를 안전하게 관리하고 발급자, 대상, 만료 시간 등 필요한 클레임을 적절히 검증해야 한다.
JWT 형식 자체가 보안을 자동으로 보장하는 것은 아니다.
참고로 JWT는 서명형(JWS)뿐 아니라 암호화형(JWE)으로도 표현할 수 있다. 여기서는 웹 인증에서 자주 접하는 서명형 JWT를 기준으로 설명하고 있다.
5. JWT를 사용하면 서버는 완전히 Stateless해질까?
JWT를 사용하면 서버가 매 요청마다 세션 저장소를 조회하지 않고 토큰 자체를 검증하는 방식으로 인증을 구현할 수 있다.
그래서 JWT가 Stateless한 인증 방식에 적합하다는 설명을 자주 접한다.
하지만 이 또한 절대적인 것은 아니다.
예를 들어 사용자가 로그아웃했다고 가정하자.
서버에 저장된 세션을 사용하는 구조라면 해당 세션을 무효화할 수 있다.
반면 서명된 JWT만으로 인증을 수행하는 시스템에서는, 이미 발급된 토큰이 만료되기 전까지 유효할 수 있다.
이를 해결하기 위해 토큰 만료 시간을 짧게 설정하거나, 서버 측 폐기 목록, 토큰 버전, Refresh Token 관리 등의 전략을 사용할 수 있다.
결국 인증 상태를 보다 세밀하게 제어하려면 서버 측 상태 관리가 다시 필요해질 수 있는 것이다.
세션과 JWT 비교
| 구분 | 서버 세션 방식 | 서명형 JWT 방식 |
|---|---|---|
| 주요 인증 정보 | 서버 세션 저장소 | 토큰의 검증 가능한 클레임 |
| 요청 처리 | 세션 식별자로 상태 조회 | 서명 및 클레임 검증 |
| 즉시 무효화 | 서버에서 처리하기 비교적 쉬움 | 별도 전략이 필요할 수 있음 |
| 주요 고려사항 | 세션 저장소 관리 | 토큰 탈취, 만료, 폐기 |
둘 중 어느 방식이 무조건 우수한 것은 아니다.
서비스의 요구사항과 인프라 구조에 따라 선택해야 한다.
6. 인증에서 중요한 것은 무엇을 신뢰하는가다
처음 질문으로 돌아가 보자.
비밀번호 없이도 로그인할 수 있을까?
정확히 말하면, 사용자가 이미 인증에 성공했다는 사실을 증명하는 유효한 자격 증명이 있다면 매 요청마다 비밀번호를 다시 입력할 필요는 없다.
그러나 그 자격 증명이 탈취된다면 공격자도 동일한 권한으로 접근을 시도할 수 있다.
인증(Authentication)은 사용자가 누구인지 확인하는 과정이고, 인가(Authorization)는 해당 사용자가 어떤 작업을 할 수 있는지 판단하는 과정이다.
둘은 반드시 구분되어야 한다.
로그인한 사용자라는 사실만으로 모든 데이터에 접근하도록 허용해서는 안 된다.
또한 토큰이 존재한다는 사실만으로 신뢰해서도 안 된다.
유효기간, 무결성, 발급자, 접근 권한을 검증해야 한다.
보안의 핵심은 비밀번호를 매번 입력하게 만드는 것이 아니라, 서버가 무엇을 근거로 사용자를 신뢰하며 그 신뢰를 언제까지 유지할 것인지 설계하는 데 있다.
생각해 볼 문제
Q1. HttpOnly 쿠키를 사용하면 XSS로 인한 모든 보안 문제가 해결될까?
Q2. JWT의 Payload는 왜 암호화된 데이터라고 볼 수 없을까?
Q3. JWT 기반 인증 시스템에서 즉시 로그아웃을 구현하려면 어떤 전략을 고려해야 할까?