인증이 한 번 성공한 뒤에도 사용자는 여러 요청을 이어서 보낸다. 문제는 웹의 바탕이 되는 규약이 각 요청을 서로 독립적인 것으로 다룬다는 점이다. 이전 요청에서 로그인에 성공했다는 사실은 다음 요청에 저절로 이어지지 않는다. 매 요청마다 자격 증명을 다시 제출하게 할 수는 없으므로, 한 번의 로그인을 여러 요청에 걸쳐 이어 주는 장치가 필요하다. 세션 기반 인증은 이 문제를 서버가 상태를 기억하는 방식으로 푸는 가장 고전적인 접근이다.


이번 편은 무상태의 요청 흐름 위에 어떻게 상태를 얹는지, 세션 식별자와 서버 저장소가 어떤 역할을 나누어 맡는지, 그 식별자가 어떻게 오가고 왜 추측 불가능해야 하는지, 그리고 이 방식이 무엇을 얻고 무엇을 대가로 치르는지를 다룬다. 세션은 뒤에 이어질 토큰 방식과 짝을 이루는 축이므로, 두 방식을 견주기 위해서라도 세션의 원리를 먼저 정확히 붙잡아야 한다.

무상태 위에 상태를 얹기

웹의 요청 규약은 각 요청을 독립적으로 처리하도록 설계되었다. 서버는 방금 처리한 요청과 지금 도착한 요청 사이의 연결을 스스로 알지 못한다. 이 무상태의 성질은 서버를 단순하고 확장하기 쉽게 만들지만, 로그인처럼 여러 요청에 걸쳐 이어져야 하는 맥락과는 어긋난다.


세션은 이 간극을 메우기 위해 서버가 사용자별 상태를 따로 보관하는 방식이다. 로그인에 성공한 순간 서버는 그 사용자를 위한 기록을 하나 만들어 두고, 이후 요청이 올 때마다 그 기록을 찾아 요청자가 누구인지를 되살린다. 무상태 흐름 자체는 그대로 두고, 서버 쪽에 기억을 심어 연속성을 만들어 내는 것이다.


이 방식의 핵심은 상태를 서버가 쥐고 있다는 점이다. 사용자의 기기에는 그 기록을 가리키는 짧은 표만 건네지고, 실제 내용은 서버 안에 남는다. 그래서 서버는 언제든 그 기록을 들여다보고, 고치고, 지울 수 있다. 상태의 주도권이 서버에 있다는 성질이 뒤에 살펴볼 여러 장점과 대가를 모두 낳는다.


정리하면 세션은 무상태 요청 흐름을 무상태로 두면서, 서버가 옆에 상태 저장소를 붙여 연속성을 흉내 내는 구조다. 요청 자체는 여전히 독립적이지만, 서버가 매 요청을 자신이 기억하는 사용자에 이어 붙이면서 하나의 이어진 방문처럼 다루게 된다.

식별자와 저장소의 역할 분담

세션 방식은 크게 두 부분으로 나뉜다. 하나는 각 세션을 가리키는 세션 식별자이고, 다른 하나는 그 식별자에 대응하는 실제 내용을 담은 서버 저장소다. 식별자는 사용자의 기기에 머물고, 내용은 서버에 머문다. 이 분리가 세션 방식의 뼈대다.


서버 저장소에는 그 사용자가 누구인지, 언제 로그인했는지, 언제까지 유효한지 같은 정보가 담긴다. 요청이 도착하면 서버는 함께 온 식별자로 이 저장소를 조회해 해당 기록을 꺼낸다. 기록이 있으면 그 안의 정보를 근거로 요청자를 확정하고, 없으면 인증되지 않은 요청으로 다룬다.


식별자는 그 자체로는 아무 의미가 없는 표에 가깝다. 사용자에 관한 정보를 담지 않고, 단지 서버 저장소의 어느 기록을 가리키는지만 나타낸다. 그래서 식별자를 들여다본다고 해서 사용자의 정보를 알아낼 수는 없다. 정보는 오직 서버 안에 있고, 식별자는 그 문을 여는 번호일 뿐이다.


이 구조는 무효화를 쉽게 만든다. 서버가 저장소에서 해당 기록을 지우면 그 세션은 즉시 무효가 된다. 사용자의 기기에 식별자가 남아 있어도, 가리키는 기록이 사라졌으므로 더는 통하지 않는다. 상태를 서버가 쥐고 있기에 언제든 세션 하나를 끊어 낼 수 있다는 점이 세션 방식의 큰 강점이다.

식별자가 오가는 길

세션 식별자는 대개 쿠키에 담겨 서버와 기기 사이를 오간다. 로그인에 성공하면 서버는 응답에 식별자를 담은 쿠키 설정을 실어 보내고, 브라우저는 이를 저장해 둔다. 이후 같은 서버로 향하는 요청마다 브라우저가 그 쿠키를 자동으로 함께 보낸다. 사용자가 매번 무언가를 첨부하지 않아도 식별자가 따라붙는 것이다.


이 자동 전송은 편리하지만 방어의 관점에서는 주의가 필요하다. 브라우저가 조건에 맞는 요청에 쿠키를 자동으로 싣기 때문에, 사용자의 의도와 무관하게 식별자가 실려 나가는 상황이 생길 수 있다. 이 성질이 뒤에 다룰 사이트 간 요청 위조의 바탕이 되므로, 세션을 쓸 때는 쿠키의 전송 조건을 함께 통제해야 한다.


식별자를 담는 쿠키에는 여러 보호 속성을 붙여야 한다. 스크립트가 쿠키를 읽지 못하게 막는 속성, 암호화된 연결에서만 전송되게 하는 속성, 다른 사이트에서 시작된 요청에는 실리지 않게 제한하는 속성이 대표적이다. 이 속성들은 다음 편에서 쿠키 자체를 다루며 자세히 살펴보지만, 세션 식별자야말로 이 보호가 가장 절실한 값이다.


식별자를 쿠키가 아닌 주소나 본문에 실어 나르는 방식은 피해야 한다. 주소에 식별자가 실리면 기록에 남거나 화면에 노출되어 새어 나가기 쉽다. 쿠키는 이런 노출을 줄이면서 자동 전송의 편의를 함께 제공하므로, 세션 식별자의 운반 수단으로 자리 잡았다. 다만 그 편의에 기대는 만큼 쿠키 자체의 보호를 소홀히 해서는 안 된다.

얻는 것과 치르는 대가

세션 방식의 가장 큰 장점은 상태를 서버가 완전히 통제한다는 데 있다. 로그아웃, 강제 만료, 특정 기기의 세션만 끊기 같은 조작이 서버 저장소를 손대는 것만으로 즉시 이루어진다. 사용자의 기기에 남은 식별자를 회수하지 못해도, 서버가 기록을 지우면 그만이다. 이 즉각적인 무효화가 세션의 대표적 이점이다.


사용자의 기기에 민감한 정보가 남지 않는 것도 장점이다. 기기에는 의미 없는 식별자만 있고 실제 정보는 서버에 있으므로, 기기 쪽에서 정보가 새어 나갈 여지가 줄어든다. 서버가 정보를 쥐고 있는 만큼 그 정보의 보호도 서버 한곳에 집중할 수 있다.


대가는 서버가 상태를 짊어진다는 점이다. 로그인한 사용자 수만큼 세션 기록이 쌓이고, 서버는 이를 저장하고 조회할 수단을 갖추어야 한다. 사용자가 늘수록 저장소의 부담도 늘어난다. 매 요청마다 저장소를 조회하는 비용도 따르므로, 저장소의 속도가 전체 응답 속도에 영향을 준다.


서버가 여러 대로 나뉘면 이 상태를 어떻게 공유할지가 또 다른 과제가 된다. 어느 서버가 요청을 받든 같은 세션을 조회할 수 있어야 하므로, 세션 저장소를 여러 서버가 함께 바라보는 별도의 공용 저장소로 두는 구성이 흔히 쓰인다. 상태를 서버가 쥐는 대가로, 그 상태를 어디에 두고 어떻게 나눌지를 늘 함께 설계해야 한다.

추측 불가능한 식별자

세션 방식의 안전은 식별자를 남이 알아내지 못한다는 전제 위에 서 있다. 식별자를 손에 넣은 쪽은 그 세션의 주인 행세를 할 수 있으므로, 식별자는 결코 추측 가능한 값이어서는 안 된다. 순서대로 늘어나는 번호나 짧고 규칙적인 값은 남이 다음 값을 짐작하거나 무작위로 맞혀 볼 여지를 준다.


그래서 식별자는 충분히 길고 예측 불가능한 무작위 값으로 만들어야 한다. 값의 공간이 넓어 무작위로 맞히는 시도가 사실상 불가능한 수준이어야 하고, 값을 만드는 근원도 예측 가능한 규칙이 아니라 안전한 난수여야 한다. 이 두 조건이 갖추어져야 식별자를 훔치지 않는 한 세션을 가로챌 수 없다.


식별자가 새어 나가는 경로도 함께 막아야 한다. 스크립트가 쿠키를 읽지 못하게 하고, 암호화된 연결로만 오가게 하며, 기록이나 주소에 식별자가 남지 않도록 하는 것이 그 방어다. 아무리 값을 잘 만들어도 전송과 저장 과정에서 새어 나가면 소용이 없으므로, 값의 강도와 전송의 보호는 함께 가야 한다.


또 하나 조심할 것은 로그인 전후로 식별자를 바꾸는 일이다. 로그인하기 전에 이미 발급된 식별자를 로그인 후에도 그대로 쓰면, 미리 심어 둔 식별자로 남의 로그인을 가로채는 공격에 노출될 수 있다. 그래서 인증에 성공하는 순간 서버는 새 식별자를 발급하고 이전 것을 폐기해야 한다. 권한이 올라가는 시점마다 식별자를 새로 발급하는 것이 이 계열의 공격을 막는 기본 방어다.

세션의 수명과 정리

세션에는 수명을 정해 두어야 한다. 로그인한 세션이 영원히 유효하면, 한 번 새어 나간 식별자가 언제까지고 통하게 된다. 그래서 세션에는 마지막 활동 이후 일정 시간이 지나면 만료되는 유휴 만료와, 발급 시점부터 일정 시간이 지나면 무조건 만료되는 절대 만료를 함께 두는 것이 안전하다.


유휴 만료는 사용자가 자리를 비운 세션을 자동으로 정리한다. 공용 기기에 로그인한 채 자리를 떴을 때 그 세션이 방치되는 위험을 줄여 준다. 절대 만료는 활동이 이어지더라도 세션이 무한정 살아 있지 못하게 상한을 둔다. 두 만료가 함께 작동하면, 방치된 세션과 지나치게 오래된 세션이 모두 걸러진다.


만료된 세션의 기록은 저장소에서 제때 지워야 한다. 만료 표시만 해 두고 실제로 지우지 않으면 저장소에 죽은 기록이 쌓인다. 그래서 만료 시점을 검사해 오래된 기록을 정리하는 절차를 함께 갖추어야, 저장소가 무한정 커지는 것을 막을 수 있다. 세션을 다룬다는 것은 만드는 일뿐 아니라 끝내고 치우는 일까지 포함한다.


민감한 동작 앞에서는 세션이 유효하더라도 다시 확인을 요구하는 설계도 유효하다. 비밀번호 변경이나 결제처럼 위험이 큰 동작에서는, 세션이 살아 있다는 것만으로 통과시키지 않고 자격을 한 번 더 확인한다. 세션의 편의에 기대되 위험이 큰 지점에서는 관문을 한 겹 더 두는 것이다.


여러 기기와 동시 세션

한 사용자가 여러 기기에서 동시에 로그인하는 상황은 흔하다. 세션 방식에서는 각 로그인마다 별도의 세션 기록이 서버 저장소에 생기므로, 한 사용자가 여러 개의 살아 있는 세션을 동시에 가질 수 있다. 이 여러 세션을 어떻게 다룰지가 운영과 방어 모두에서 하나의 결정 지점이 된다.


서버가 상태를 쥐고 있다는 성질은 여기서도 힘을 발휘한다. 한 사용자에게 딸린 세션 기록을 모두 조회할 수 있으므로, 지금 어느 기기들에서 로그인되어 있는지를 사용자에게 보여 줄 수 있다. 낯선 기기의 세션이 목록에 있으면 사용자가 그것을 알아채고 끊을 수 있다. 이런 세션 목록과 개별 종료 기능은 계정 도용을 사용자 스스로 차단하게 돕는 방어 수단이 된다.


비밀번호를 바꾸거나 도용이 의심되는 순간에는 그 사용자의 모든 세션을 한꺼번에 끊는 처리가 필요하다. 저장소에서 해당 사용자의 세션 기록을 모두 지우면, 어느 기기에 남은 식별자도 더는 통하지 않는다. 이 일괄 무효화는 상태를 서버가 쥐는 세션 방식에서 특히 깔끔하게 이루어진다. 토큰 방식이라면 같은 일을 하기 위해 별도의 폐기 장치를 두어야 한다.


동시 세션의 수에 상한을 두는 정책도 쓰인다. 한 계정에 지나치게 많은 세션이 동시에 살아 있으면 그중 하나가 새어 나간 것일 수 있으므로, 오래된 세션을 자동으로 밀어내거나 새 로그인 시 이전 세션을 정리하는 방식으로 표면을 좁힌다. 여러 세션을 허용하되 그 수와 상태를 서버가 늘 파악하고 있는 것이 방어의 기본이다.

정리하면 세션 기반 인증은 무상태 요청 위에 서버가 상태 저장소를 붙여 연속성을 만드는 방식으로, 의미 없는 식별자가 기기에 머물고 실제 정보는 서버에 남는다. 서버가 상태를 쥐므로 즉각적인 무효화가 쉬운 대신 저장소의 부담과 공유 과제를 떠안으며, 식별자는 추측 불가능해야 하고 전송 경로도 함께 보호되어야 한다. 다음 편에서는 이 식별자를 실어 나르는 쿠키 자체를 자세히 들여다본다.