Logo

쿠키 2부: 세션은 쿠키가 필요해~

“사용자 인증을 할 때 쿠키를 사용하면 위험하고요 서버에 데이터를 저장하는 세션을 사용하는 것이 안전해요.”

사용자 인증에 대해서 논할 때 자주 듣게 되는 얘기인데요. 과연 이 말이 맞는 말일까요? 저한테는 굉장히 모순된 얘기로 들리는 것 같습니다.

많은 분들이 쿠키(cookie)와 세션(session)을 서로 대립하거나 세션이 쿠키를 대체하는 기술로 오해하는 것 같은데요. 사실 쿠키와 세션은 상호 보완을 하는 기술이라고 보는 것이 더 맞을 것입니다.

지난 포스팅에서는 서버가 브라우저에 쿠키를 어떻게 저장하고 쿠키라는 기술의 한계에 대해서 알아보았는데요. 하지만 이러한 갖가지 단점에도 불구하고 쿠키는 여전히 웹에서 중요한 한 축을 담당하며 다양하게 사용되고 있습니다.

이번 포스팅에서는 웹 개발에서 쿠키를 사용할 수 밖에 없는 결정적인 이유와 세션 기반 사용자 인증에 대해서 알아보겠습니다.

HTTP 프로토콜의 특징

HTTP 프로토콜은 연결이 유지시키지 않고(connectionless) 상태가 없는(stateless) 특성을 가지고 있습니다. 즉, 서버가 클라이언트의 요청에 응답을 하는 순간 HTTP 연결은 끊어지며, 클라이언트에서 새로운 요청을 해야 다시 HTTP 연결이 맺어지게 됩니다. (하지만 브라우저는 응답된 데이터를 화면에 여전히 보여주고 있기 때문에 일반 사용자 입장에서 브라우저와 서버 간에 연결이 끊어졌다고 느끼지는 않죠…)

이러한 HTTP 프로토콜의 특징은 웹에서 애플리케이션을 구현하는데 큰 걸림돌로 작용하게 됩니다. 상태가 없다라는 것은 각각 요청이 독립적으로 취급되어 여러 페이지에 걸쳐 흐름이 이어져야하는 서비스를 구현하기 어려워지기 때문입니다.

간단한 웹사이트가 아닌 이상 대부분의 서비스에서는 하나의 브라우저로 부터 순차적으로 들어오는 여러 개의 요청이 동일한 사용자로 부터 오는 것이라는 것을 알아야 할텐데요. 클라이언트와 연결이 유지되지 않는 상황에서 동시에 서버로 유입되는 수많은 요청이 각각 어느 사용자의 것인지 판단하는 것은 서버 입장에서 매우 힘든 일일 것입니다.

여기서 쿠키의 지속성이 빛을 발휘할 수 있는데요. 바로 서버가 쿠키를 한 번 브라우저에 저장하면 브라우저는 해당 쿠키를 매 요청마다 계속해서 서버로 돌려 보낸다는 것입니다. 다시 말해 서버가 브라우저에 쿠키 하나만 심어 놓으면 그 후로 브라우저는 성실하게 매번 서버를 방문할 때 마다 해당 쿠키를 다시 가져옵니다.

이러한 쿠키의 특성을 활용하면 서버는 각 요청이 어느 브라우저에서 오는 것인지 어렵지 않게 판단할 수 있습니다.

예를 들어, 사용자가 서비스에 최초로 접속했을 때 서버가 브라우저에게 a=1 쿠키를 저장하라고 시키면,

HTTP요청
GET /index.html HTTP/1.1
Host: www.test.com
HTTP응답
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: a=1

해당 브라우저는 사용자가 www.test.com이라는 도메인에 머무는 한 /index.html을 방문하든 /about.html을 방문하든 /contact.html을 방문하든 매번 같은 쿠키를 돌려줍니다.

HTTP요청
GET /index.html HTTP/1.1
Host: www.test.com
Cookie: a=1
HTTP요청
GET /about.html HTTP/1.1
Host: www.test.com
Cookie: a=1
HTTP요청
GET /contact.html HTTP/1.1
Host: www.test.com
Cookie: a=1

그러므로 서버 입장에서는 a=1 쿠키를 들고 들어오는 요청은 모두 이 브라우저로 부터 오는 것이구나라고 쉽게 알 수 있습니다. (물론 다른 브라우저에게는 a=1 대신에 a=2, a=3와 같은 다른 쿠키를 응답해줘야겠죠?)

부하 분산을 위한 쿠키 활용

쿠키는 대규모 서비스에서 사용자 기반으로 부하 분산(load balancing)이 필요할 때 큰 역할을 할 수 있습니다.

수많은 사용자들로 부터 동시에 들어오는 요청을 처리하려면 일반적으로 여러 대의 서버를 운영할 수 밖에 없는데요. 이런 경우 서버 앞 단에 로드 밸런서(load balancer)를 두고 부하가 여러 대의 서버로 골고루 분산될 수 있도록 인프라를 구성합니다.

이러한 환경에서 로드 밸런서가 서비스로 들어오는 요청을 순차적으로 (round-robin) 또는 랜덤하게 서버에 배정하면 어떻게 될까요? 만약에 사용자의 세션 정보가 각 서버 단위로 관리되고 있다면 사용자를 로그인된 상태로 유지하는 것이 매우 어려울 것입니다.

즉, 1번 서버에서 세션을 생성한 사용자가 다음 요청 때, 2번 서버에 붙게된다면, 서비스는 이 사용자를 로그인된 사용자로 인식할 수 없을 것입니다. 왜냐하면 2번 서버에는 해당 사용자의 세션 정보가 존재하지 않을 것이기 때문입니다.

이 경우, 쿠키를 활용하면 어렵지 않게 사용자를 고려하여 부하 분산을 할 수가 있는데요.

예를 들어, 사용자가 서비스에 최초로 접속했을 때 그 요청을 처리한 서버가 자신의 식별자를 쿠키로 저정하라고 브라우저에 시키면,

HTTP요청
GET /index.html HTTP/1.1
Host: www.test.com
HTTP응답
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: serverId=1

해당 브라우저는 사용자가 해당 사이트의 어느 페이지를 방문하든 serverId=1 쿠키를 돌려보낼 것입니다.

HTTP요청
GET /index.html HTTP/1.1
Host: www.test.com
Cookie: serverId=1
HTTP요청
GET /about.html HTTP/1.1
Host: www.test.com
Cookie: serverId=1
HTTP요청
GET /contact.html HTTP/1.1
Host: www.test.com
Cookie: serverId=1

그럼 로드 밸런서(load balancer)는 이 서버 식별자를 보고 이 브라우저에서 오는 요청을 항상 동일한 1번 서버로 보내줄 수 있을 것입니다.

참고로 이렇게 사용자 기반 부하 분산은 주로 소규모의 서비스에서 볼 수 있으며, 대규모 서비스에서는 서버 간에 공유가 가능한 별도의 데이터베이스에 세션 정보를 관리하는 경우가 많습니다.

세션 기반 사용자 인증

쿠키의 최대 단점이 뭐였죠? 맞습니다! 쿠키는 브라우저에 저장하기 때문에 유실/변조/도난되기 쉽습니다.

그럼 쿠키 대비 세션의 장점은 뭐죠? 네! 세션은 서버 측에 관리 되기 때문에 위와 같은 위험이 적습니다.

그럼 도대체 서버 측에 생성된 수많은 세션이 어떤 사용자의 것인지 어떻게 알 수 있을까요?

예를 들어, 잠시 보안은 잊어버리고 사용자의 인증 정보를 쿠키로 저장한다고 가정해볼께요.

사용자가 브라우저에서 로그인 페이지를 열고 인증 정보를 입력한 후 서버에 전송합니다.

HTTP요청
POST /login HTTP/1.1
Host: www.test.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

username=test&password=1234

그럼 서버는 사용자 DB를 조회해서 인증 정보를 검증하고 응답할 때 인증 정보를 쿠키로 브라우저 보내겠죠?

HTTP응답
HTTP/1.1 302 Found
Content-Type: text/html
Location: https://www.test.com/
Set-Cookie: username=testSet-Cookie: password=1234

그러면 브라우저는 매번 요청을 보낼 때 마다 이 인증 정보가 담긴 쿠키를 서버로 돌려 보낼 것입니다.

HTTP요청
GET / HTTP/1.1
Host: www.test.com
Cookie: username=test; password=1234

그럼 서버에서 사용자 인증을 위해서 할 일은 이 쿠키로 넘어온 인증 정보가 사용자 DB에 존재하는지 확인하는 것 밖에 없는데요.

이번에는 사용자가 입력한 인증 정보를 세션으로 저장하는 시나리오를 생각해볼까요?

동일하게 사용자가 브라우저에서 로그인 페이지를 열고 인증 정보를 입력 후 전송합니다.

HTTP요청
POST /login HTTP/1.1
Host: www.test.com
Content-Length: 27
Content-Type: application/x-www-form-urlencoded

username=test&password=1234

서버는 사용자 DB를 조회해서 인증 정보를 검증하고 서버에 세션을 생성합니다.

자 이제부터 사용자는 브라우저로 웹사이트의 이 페이지 저 페이지를 방문할텐데 어떻게 로그인 상태로 유지시키죠?? 사용자한테 새로운 페이지를 방문할 때 마다 다시 로그인하라고 할 수도 없을텐데요…

네… 여기서 쿠키가 구원 투수로 등판합니다! 🍪

서버에서 세션을 생성하면 그 세션을 식별할 수 있는 식별자를 얻을 수 있겠죠? 보통 이것을 세션 아이디라고 부르는데요. 이 세션 아이디를 쿠키로 브라우저에 응답해주면 어떨까요?

HTTP응답
HTTP/1.1 302 Found
Content-Type: text/html
Location: https://www.test.com/
Set-Cookie: sessionId=a2dd774e36e2

그러면 브라우저는 매번 요청을 보낼 때 마다 이 세션 아이디가 담긴 쿠키를 서버로 돌려 보낼 것입니다.

HTTP요청
GET / HTTP/1.1
Host: www.test.com
Cookie: sessionId=a2dd774e36e2

이제 서버에서는 이 세션 아이디에 해당하는 세션이 존재하는지만 확인해주기만 하면 되겠네요.

위 예시는 어디까지나 전반적인 개념을 위해서 단순화되었으며 물론 실전에서는 세션 아이디를 담은 쿠키에 유효 기간도 설정하고 적용 범위도 제한하고 보안 속성도 추가하겠지요? (관련 내용은 이전 포스팅에서 자세히 다루었으니 참고 바랍니다.) 적어도 브라우저에 인증 정보를 저장하지 않았으니 클라이언트 컴퓨터가 털리더라도 인증 정보가 유출될 일은 없겠네요.

참고로 예전에는 세션 아이디를 담는 쿠키의 이름을 보고 해당 서비스가 어떤 언어로 작성되었는지 유추하기가 쉬웠어요. 예를 들어, Java에서는 JSESSIONID가 많이 사용되고, PHP에서는 PHPSESSID, ASP .NET에서는 ASP.NET_SessionId가 많이 사용되었죠. 하지만 요즘에는 보안을 위해서 이렇게 널리 알려진 세션 아이디를 사용하지 않는 추세입니다.

악용될 수 있는 쿠키

지금까지 브라우저가 쿠키를 지속적으로 서버로 보낸다는 특성을 활용한 대표적은 응용 사례를 살펴보았는데요. 안타깝게도 이러한 쿠키의 지속성이 항상 좋은 방향으로만 쓰이지는 않고 있어요.

브라우저로 웹사이트에 접속할 때 그 웹사이트의 도메인이 아닌 타 도메인을 상대로 적용되는 쿠키가 넘어오는 경우가 많은데요. 소위 이런 쿠키를 서드파티(third party) 쿠키라고 부르죠. 보통 사용자 맞춤 광고를 위해서 사용자가 어떤 웹사이트를 방문하는지 추척하는 용도로 많이 사용이 되고 있습니다.

예를 들어, 브라우저에서 페이스북(Facebook)을 열면 보통 응답에 다음과 같이 서드파티 쿠키가 담겨 있는 것을 확인하실 수 있을 거에요.

HTTP응답
HTTP/1.1 200 OK
set-cookie: fr=0ujkuQJaMvRE4sH0P..BlEF3H.xG.AAA.0.0.BlEF3H.AWXq9qKoMq8; domain=.facebook.com; path=/; Max-Age=7776000; secure; httponly
set-cookie: sb=x10QZckUJHZ-_0UeslPeBasy; domain=.facebook.com; path=/; Max-Age=34560000; secure; httponly

또한 좀비 쿠키라고 해서 유효 기간을 엄청 길게 하거나 자바스크립트를 이용한 다른 편법들을 써서 아무리 브라우저에서 삭제를 해도 계속 부활하는 쿠키들도 있어요. 이러한 나쁜 쿠키에 대해서 까지 본 포스팅에서 깊게 다룰 수는 없지만 관심이 있으시면 검색을 해보시면 좋을 것 같습니다.

마치면서

쿠키에 대해서 이렇게 긴 글을 쓰게 될 줄을 몰랐는데 쓰다보니 2부에 걸쳐서 쿠키에 대해서 다루게 되었네요 😁 제 글이 쿠키에 대해서 막연하게 아시던 분들에게 조금이라도 도움이 되었으면 좋겠습니다 🙏