HTTP의 Stateless
HTTP 프로토콜은 기본적으로 요청 ↔ 응답을 진행한 후 이전 통신에 대한 상태를 저장하지 않는 Stateless한 구조를 가지고 있다
HTTP가 Stateless함에 따라 다음과 같은 이점이 존재한다
- 상태를 유지하기 위한 오버헤드가 줄어든다
- 상태를 유지하지 않기 때문에 확장성(Scale Out)이 높다
하지만 어느 특정 상황에서는 Client측의 여러 정보들을 Server측에서 기억해야 할 필요가 있어졌고 Client-Server간의 상태를 Stateful하게 유지해야 할 필요성이 높아졌다
- 특정 사용자 접속 제한
- 사용자에 따른 추천 시스템
- ...
쿠키
쿠키는 HTTP의 Stateless함을 보완하기 위해서 Client측에 일시적/영구적으로 저장되는 정보이다
특징
- 웹 브라우저 쿠키 저장소에 key - value 쌍 구조로 저장된다
- Server는 쿠키를 발급해줄 때 쿠키의 유효 시간을 명시해서 응답할 수 있다
- 세션 쿠키: 유효 시간을 명시하지 않은 쿠키 (브라우저 닫으면 제거)
- 영속 쿠키: 유효 시간을 명시한 쿠키 (브라우저를 닫아도 시간동안은 유지)
- Server가 쿠키를 발급해준다면 Client는 별도로 쿠키를 조작하지 않아도 매 요청마다 쿠키가 헤더에 실려서 전달된다
옵션
1. HttpOnly
HttpOnly가 적용된 쿠키는 웹 브라우저에서만 접근 가능하도록 제한된다
- 이 말은 즉 스크립트를 통해서 쿠키에 접근해서 제어할 수 없다는 의미이다
- 스크립트를 통해서 접근할 수 없기 때문에 XSS(Cross-Site Scripting) 공격을 방지할 수 있다
- XSS = 권한이 없는 사용자가 웹 사이트에 악의적인 용도로 스크립트를 삽입
Set-Cookie: {name}={key}; …; HttpOnly;
2. Secure
HTTPS(HTTP + SSL/TLS) 통신일 경우에만 쿠키를 전송하는 옵션이다
Set-Cookie: {name}={key}; …; HttpOnly; Secure;
3. Domain
설정한 도메인을 대상으로한 요청에만 쿠키가 전송된다
Set-Cookie: {name}={key}; …; Domain=study-with-me.co.kr;
- study-with-me.co.kr 도메인에 대해서만 쿠키를 전송할 수 있다
Domain 옵션은 단 하나의 도메인만 적을 수 있고 해당 도메인의 서브 도메인에서는 쿠키를 공유하지 않는다
4. SameSite
None
- Cross-Site 요청의 경우에도 상관없이 항상 쿠키를 전송하는 모드
Strict
- Cross-Site 요청의 경우 항상 전송할 수 없는 모드
Lax
- 대체적으로 Third Party Cookie는 전송되지 않지만 예외적인 요청에는 전송
Set-Cookie: {name}={key}; …; SameSite={None / Strict / Lax}
5. Expires
Cookie가 언제까지 유효한지 명시한다
- Expires를 생략한다면 세션이 만료될 때 쿠키도 함께 만료된다
Set-Cookie: {name}={key}; …; Expires=Tue, 19 Jan 2038 03:14:07 GMT
6. Max-Age
Cookie의 유효 시간을 명시한다
- 발급한 시점으로부터 Max-Age가 지나면 만료된다
- Expires와 함께 쓰면 Max-Age가 우선적으로 적용된다
Set-Cookie: {name}={key}; …; Max-Age=3600
쿠키의 문제점
하지만 이러한 쿠키에도 문제점은 존재한다
일단 쿠키는 한번 생성되면 매 요청마다 HTTP Request에 실려서 전송된다
만약 쿠키에 저장된 정보가 많다면 매 요청마다 그만큼의 오버헤드가 발생하게 되는 것이다
브라우저마다 다르지만 일반적으로 이러한 문제점으로 인해 쿠키의 데이터는 4KB로 제한된다
또한 인터넷 익스플로러를 제외한 다른 브라우저의 경우 사이트당 쿠키의 개수도 20개로 제한된다
쿠키는 성능상 문제뿐만 아니라 보안적인 문제점 또한 존재한다
- 위에서 말했듯이 쿠키는 Client가 보관하는 개념이다
- 쿠키에 만료시각을 명시하지 않는다면 Client가 브라우저를 종료하는 순간 해당 쿠키는 없어진다
- 하지만 만료시각을 명시하였다면 그 시간 동안은 휘발되지 않는다
- 만약 쿠키에 굉장히 민감한 정보가 파일의 형태로 존재하게 된다면 Attacker에 의해 탈취되기 굉장히 쉬워진다
이러한 문제점을 해결하기 위해서 세션이라는 개념이 등장하게 되었다
세션
위에서 살펴본 Cookie는 웹 브라우저에서 사용자 정보를 저장하고 관리한다
세션은 쿠키와 다르게 Server측에 사용자 정보를 저장하고 관리하는 방식이다
- 서버에서 Client를 위한 고유 세션 ID를 부여해서 브라우저를 종료할때까지 인증상태를 유지한다
- 세션 ID를 쿠키에 저장하기는 하지만 쿠키에 저장되는 값이 사용자에 대한 정보가 아닌 고유 세션 ID임에 따라 사용자 정보가 그대로 노출될 일은 없다
사용자 정보 자체를 안전한 서버측에서 관리하기 때문에 쿠키보다 보안적으로 우수하다고 할 수 있다
세션 저장
일반적으로 세션 데이터는 서버의 메모리에 저장된다
이 문장을 통해서 알 수 있는 점은 세션이 많아지면 많아질수록 서버의 부하는 점점 커진다는 것이다
기존에 하나의 서버로 운영을 하고 있을 경우 서비스가 커질수록 해당 서버에 대해서 부하가 집중된다
이 시점에 Scale Out + 로드 밸런싱을 통해서 특정 서버에 집중되던 부하를 분산시킨다
세션 불일치
세션 불일치는 단일 서버에서는 걱정하지 않아도 된다
하지만 서버를 Scale Out하게 되면 이는 단일 서버가 아닌 다중서버가 된다
- 기본적으로 세션은 서버 메모리에 저장된다
- 하지만 서버 자체가 Scale Out된다면 ServerA에서 발급한 세션이 ServerB에는 없을 수 있다
이러한 문제를 세션 불일치라고 한다
1. Sticky Session
Sticky Session은 간단하게 말하면 Client의 첫 요청에 대해서 응답을 준 Server에 대해서 이후 모든 요청을 해당 Server로 보내는 방식이다
위에서 말한 로드 밸런싱에 의한 문제를 해결할 수는 있지만 Sticky Session 또한 단점이 존재한다
첫번째로 로드 밸런싱의 효율이 제대로 나타나지 않을 수 있다
- 왜냐하면 로드 밸런싱이라는 개념 자체가 특정 서버에 부하를 주지 않기 위해서 요청을 Scale Out을 통해서 여러 서버로 분산시키는 개념이다
- 하지만 Sticky Session을 통해서 특정 서버에 부하를 줄 수 있게 된다
- 최악의 경우 N개의 서버가 존재하지만 1개의 서버에만 요청이 계속 들어갈 수 있다
두번째로 결국 세션이 특정 서버에 존재하는데 만약 해당 서버가 죽게된다면 세션은 더이상 의미가 없어진다
이러한 2가지 문제점은 결국 세션이라는 개념 자체가 한 곳에 집중되어서 발생한 문제이다
이를 해결하기 위해서 나타난 개념이 Session Clustering이다
2. Session Clustering
Session Clustering은 특정 서버에서 세션이 생성될때마다 다른 서버로 전파 & 복제하는 방식이다
그에 따라 특정 서버로만 트래픽이 몰릴 일도 없고 하나의 서버가 죽더라도 세션 정보 자체를 잃어버릴 일도 없다
하지만 대규모 클러스터에는 Session Clustering 방법 또한 문제가 존재한다
- 세션을 모든 서버가 복제해서 가지고 있기 때문에 메모리를 비효율적으로 사용하게 된다
- 세션을 생성할때마다 전파 → 복제를 거치기 때문에 네트워크 트래픽이 많이 발생한다
- 전파 → 복제 과정에서 시간차로 인한 불일치가 발생할 수 있다
그리고 새로운 서버를 만들때마다 기존의 세션 데이터를 옮겨서 Clustering해야하는 번거로움 또한 존재한다
이러한 번거로움을 없애기 위해서 세션만을 관리하기 위한 외부 Session Server가 등장하게 된다
3. Session Server 분리
Session Server 자체를 분리하게 되면 결국 WAS는 분리된 세션 서버에서 세션을 검증하면 된다
하지만 이러한 Session Server 또한 서버가 죽어버리면 모든 세션이 날라가게 된다
쿠키 & 세션을 통해서 HTTP의 Stateless한 특징으로 인해 발생하는 번거로움을 해결한듯하다
하지만 Stateful하다면 서버의 Scale Out에 약간의 제약이 생기고 메모리상의 문제도 발생하게 된다
- 확장성을 위해 Stateless한 특성도 살리고 메모리상의 문제도 없애기 위한 방법은 Token을 사용하는 것이다
Token
위에서 살펴본 세션 방식은 사용자 정보를 서버 메모리에서 관리하는 방식이다
그러나 단순하게 세션을 관리하게 된다면 위에서 본 것처럼 세션 불일치라는 문제가 발생하게 된다
이렇게 된다면 세션 불일치를 해결하기 위한 또 다른 방안을 고민해야 하기 때문에 서버의 확장성 측면에서 봤을 때 자유롭다고 할 수 없다
반면 토큰은 인증 정보를 Client가 들고 있고 이러한 토큰은 마패라고 생각하면 된다
- 회사의 출입증 개념
토큰의 대표적인 유형은 JWT(Json Web Token)이 있고 서버에서 토큰을 발급해줄 때 전자서명을 통해서 토큰의 무결성을 확보하고 Client의 요청으로 들어온 토큰의 내용이 위변조 되었는지 서버측의 Secret Key로 확인한다
- JWT의 형식과 자세한 내용에 대해서는 별도로 다루지 않고 Client - Server간의 인증 및 인가 처리에 활용되는 Access Token & Refresh Token에 대해서 다룰 예정이다
Access Token
토큰 방식의 인증? 처리는 다음과 같다
- Client가 Server에 인증 정보를 보낸다
- Server에서는 인증을 진행한다
- 인증이 성공적으로 진행됐다면 Server에서는 Access Token을 발행해서 Client에게 발급한다
- 이후 Client의 모든 자원 요청에는 Access Token을 포함한다
Token기반의 인증 방식은 HTTP의 Stateless한 특성을 지킬 수 있다
그 말은 즉, Server는 발급한 토큰에 대한 제어권이 없다는 의미이다
이 말을 다르게 해석해보면 Client가 모든 제어권을 가지고 있고 토큰에 대한 관리를 제대로 하지 않을 경우 Attacker에 의해 탈취될 수 있다
추가적으로 Access Token을 통해서 인증을 처리한다는 말은 약간 모순적이라고 생각한다
왜냐하면 Token 기반으로 처리를 할 경우 Client - Server간의 관계는 Stateless하다
따라서 Server는 Token을 발급해주고 들어오는 Token에 대해서 유효성 검증을 진행하고 자원을 응답해줄뿐 그에 대한 인증은 사실 할 수 없다
발급된 Access Token은 인증이 필요한 자원에 대한 권한을 나타낼뿐 Token을 담아서 보낸 Client가 정상적인 사용자인지 Attacker인지까지는 확인할 수 없다는 것이다
만약 Access Token이 탈취되었다면 어떤 일이 발생할까?
- Access Token이 아직 만료되지 않았다면 탈취를 성공한 Attacker는 서버의 자원에 마음껏 접근할 수 있게 된다
- 결국 서버는 Access Token의 만료를 기다리는 방법 외에 다른 방법이 존재하지 않는다
- 이에 대한 해결책으로 Access Token의 유효기간을 굉장히 짧게 가져가는 방법이 있다
- 하지만 만약에 Attacker가 아닌 정상적인 사용자가 서비스를 이용한다고 해도 굉장히 짧은 시간 내에 연속적으로 서버에 인증을 받아서 Access Token을 계속해서 재발급받아야 하는 문제점이 존재한다
이러한 문제를 해결하기 위해서 Refresh Token이라는 개념이 필요하다
Refresh Token
Refresh Token의 목적은 단순하다
Access Token의 재발급
결론적으로 말하자면 Access Token의 유효 기간을 짧게 잡아서 탈취되더라도 그에 대한 피해를 최소화시킨다
그 후 무효된 Access Token의 재발급을 위해서 Refresh Token을 활용한다
- Access Token = 자원에 대한 접근을 위한 토큰
- Refresh Token = Access Token 재발급을 위한 토큰
Refresh Token은 Access Token에 비해 꽤 긴 유효 기간을 갖게된다
하지만 Refresh Token자체로 안전이 확보된 것은 아니다
- Attacker는 Refresh Token을 탈취하고 Refresh Token 유효기간내에서 무한으로 Access Token을 발급받을 수 있다
이러한 문제를 해결하기 위해서 RTR(Refresh Token Rotation)이라는 개념이 등장하였다
RTR (Refresh Token Rotation)
RTR은 Refresh Token을 1회성으로 사용할 수 있게 하는 것이다
- Refresh Token을 통해서 Access Token을 재발급 받을 때 Refresh Token도 함께 재발급 받게 된다
RTR을 통해서 서버측에서는 이미 활용된 Refresh Token에 대한 Access Token 재발급 요청이 왔을 때 해당 요청을 거부할 수 있고 그에 따른 조치를 취할 수 있다
Refresh Token을 통해서 Access Token의 유효기간을 굉장히 짧게 가져가더라도 그에 대한 부담이 적어지지만 그 짧은 기간내에서도 Attacker는 Access Token을 탈취한 후 악용할 수 있다
그리고 Refresh Token 그 자체도 탈취될 수 있다
물론 RTR을 통해서 일회성 Refresh Token을 만들고 이미 사용자가 Refresh Token을 활용했다면 문제가 해결되지만 만약 활용하지 않고 Attacker에게 탈취당한다면 1회만으로도 Access Token을 발급받을 수 있게 된다
- 따라서 Client는 Access Token & Refresh Token에 대한 관리를 철저히 해서 안전한 곳에 보관해야 한다
세션 vs 토큰
1. 크기
세션 방식의 경우 세션 ID만 쿠키에 실어서 응답하면 되기 때문에 트래픽에 대한 부담이 그렇게 심하지 않다
하지만 토큰(JWT 형식)의 경우 토큰 Payload에 얼마나 많은 데이터가 들어있냐에 따라 그만큼 인코딩한 문자열을 헤더에 실어서 보내기 때문에 세션 방식에 비해 트래픽 부담이 크다고 볼 수 있다
2. 안전 & 보안
세션 방식의 경우 사용자 인증 정보 자체를 서버에서 관리하기 때문에 서버가 직접적으로 털리지 않는 이상 안전한 방식이라고 할 수 있다
- 물론 털리더라도 즉시 세션 ID를 무효화해서 방어할 수 있다
하지만 토큰 방식의 경우 Payload에 어떤 데이터를 담냐에 따라 다르지만 만약 민감 정보를 담았다면 토큰은 Client에서 직접 관리하는 방식이고 토큰이 털리게 된다면 Payload를 디코딩해서 볼 수 있기 때문에 보안적으로 취약하다고 할 수 있다
- 따라서 토큰 Payload에 민감한 정보는 담으면 안된다
3. 확장성
크기, 안전 & 보안적 측면에서 보면 세션이 더 좋은 인증 수단이라고 볼 수 있다
- 토큰은 세션에 비해 부하도 높고 안전 & 보안적 측면에서도 별론데
하지만 최근 웹 애플리케이션에서 토큰 방식을 활용하는 가장 큰 이유는 확장성때문이다
기본적으로 단일 서버의 부하를 덜어주기 위해서 Scale Out을 진행한다
Scale Out을 진행한 경우 세션 기반 인증 방식을 사용하면 위에서 말한것처럼 세션 불일치를 해결하기 위해서 Sticky Session, Session Clustering과 같은 여러 방안을 모색해야 한다
하지만 토큰 방식은 Scale Out되더라도 서버측에서 토큰을 트래킹하지 않고 클라이언트측에서 모든 인증 정보를 관리하기 때문에 서버에서 별도의 작업을 할 필요없이 Scale Out이 자유로워 진다
- HTTP의 Stateless하다는 점을 그대로 활용할 수 있고 그에 대한 높은 확장성을 가질 수 있다
4. 서버 부담
세션 방식은 결국 서버의 메모리에서 관리되고 사용자가 급격하게 늘어나면 서버의 메모리도 그만큼의 부하를 감당해야 한다