세션 방식은 서버가 상태를 쥐고 있어 무효화가 쉬운 대신, 사용자 수만큼 상태를 저장하고 여러 서버가 그 상태를 나누어 보아야 하는 부담을 안는다. 서비스가 여러 대의 서버로 나뉘고 다양한 클라이언트가 붙는 환경에서는 이 부담이 만만치 않다. 토큰 기반 인증은 여기서 발상을 뒤집어, 상태를 서버에 두지 않고 요청 자체에 실어 보내는 방식으로 문제를 푼다.


이번 편은 상태를 서버에 두지 않는다는 발상이 어떻게 성립하는지, 서명이 어떻게 신뢰를 만들어 내는지, 토큰과 세션이 무엇을 맞바꾸는지, 토큰을 어디에 두어야 하는지, 그리고 수명과 무효화 사이의 딜레마를 어떻게 다루는지를 살펴본다. 토큰은 세션의 대안이자 서로 다른 대가를 치르는 선택지이므로, 두 방식을 견주며 각자의 자리를 이해하는 것이 목표다.

상태를 서버에 두지 않는 발상

세션에서는 서버가 저장소에 기록을 두고, 사용자의 기기에는 그 기록을 가리키는 식별자만 건넸다. 토큰 방식은 이 구도를 바꾼다. 사용자에 관한 정보 자체를 요청에 실어 보내고, 서버는 그것을 저장하지 않는다. 요청이 도착하면 서버는 저장소를 조회하는 대신, 실려 온 토큰만 검사해 요청자를 확정한다.


이 방식에서 서버는 자신이 발급한 토큰을 따로 기억하지 않는다. 대신 토큰이 위조되지 않았는지만 확인할 수 있으면 된다. 토큰 안에 담긴 정보가 진짜 서버가 발급한 것이고 도중에 손대어지지 않았음을 확인할 수만 있다면, 서버는 그 정보를 믿고 요청자를 판별할 수 있다. 상태를 저장하는 대신 검증하는 것으로 대체하는 셈이다.


이렇게 하면 서버는 상태를 짊어지지 않아도 된다. 사용자가 아무리 늘어도 서버가 저장할 세션 기록이 없고, 여러 서버가 상태를 나누어 볼 필요도 없다. 어느 서버가 요청을 받든 토큰만 검증하면 되므로, 서버를 늘리거나 줄이기가 수월해진다. 무상태라는 웹의 본래 성질에 인증도 맞춰지는 것이다.


다만 이 발상이 성립하려면 한 가지 조건이 필요하다. 요청에 실려 온 정보가 진짜임을 서버가 저장 없이 확인할 수 있어야 한다. 이 확인을 가능하게 하는 장치가 서명이다. 서명이 없다면 요청에 실려 온 정보를 서버가 믿을 근거가 없고, 토큰 방식 자체가 성립하지 않는다.

서명이 만드는 신뢰

서명은 토큰에 담긴 정보가 서버가 발급한 그대로임을 보증하는 장치다. 서버는 토큰의 내용을 자신만 아는 비밀과 함께 계산해 서명을 만들어 토큰에 붙인다. 검증할 때는 같은 계산을 다시 해서 서명이 일치하는지 본다. 내용이 조금이라도 바뀌면 서명이 어긋나므로, 서버는 토큰이 손대어졌는지를 즉시 알아챈다.


여기서 중요한 점은 서명이 내용을 감추는 것이 아니라 위조를 막는다는 것이다. 토큰 안의 정보는 대개 그대로 읽을 수 있는 상태로 실린다. 서명은 그 정보를 숨기는 자물쇠가 아니라, 누군가 손댔는지를 드러내는 봉인에 가깝다. 그래서 토큰에는 남에게 보여도 되는 정보만 담아야 하며, 이 원리는 다음 편에서 토큰의 구조를 다루며 자세히 짚는다.


서명의 신뢰는 전적으로 비밀의 보호에 달려 있다. 서버의 비밀이 새어 나가면 남이 스스로 유효한 서명을 붙인 토큰을 만들 수 있게 되고, 그러면 서버는 그 위조 토큰을 진짜로 받아들인다. 그래서 이 비밀은 코드에 적지 않고 별도의 안전한 저장소에 두며, 유출이 의심되면 즉시 교체할 수 있도록 준비해 두어야 한다.


토큰은 대개 요청의 인증 헤더에 담겨 서버로 전달된다. 클라이언트가 토큰을 보관하고 있다가 요청을 보낼 때마다 이 헤더에 실어 보내면, 서버는 헤더에서 토큰을 꺼내 검증한다. 쿠키의 자동 전송과 달리 클라이언트가 명시적으로 토큰을 실어 보내는 방식이라, 어느 요청에 토큰을 붙일지를 클라이언트가 통제한다는 차이가 있다.

세션과 토큰이 맞바꾸는 것

세션과 토큰은 같은 목적을 다른 대가로 이룬다. 세션은 상태를 서버에 두어 무효화를 쉽게 얻는 대신 저장과 공유의 부담을 진다. 토큰은 상태를 요청에 실어 저장 부담을 덜지만, 그 대가로 무효화가 어려워진다. 어느 쪽이 낫다기보다 무엇을 더 중히 여기느냐의 문제다.


토큰의 가장 큰 이점은 서버가 상태를 짊어지지 않는다는 데 있다. 여러 서버가 저장소를 공유하지 않아도 각자 토큰을 검증할 수 있고, 서버를 자유롭게 늘리고 줄일 수 있다. 서로 다른 여러 서비스가 하나의 발급자가 낸 토큰을 각자 받아들이는 구조에서도 잘 맞는다. 확장과 분산이 필요한 환경에서 토큰이 힘을 발휘하는 이유다.


대신 무효화가 어렵다는 약점이 따른다. 서버가 토큰을 저장하지 않으므로, 한 번 발급한 토큰을 만료 전에 즉시 무효로 만들 손잡이가 없다. 세션이라면 저장소의 기록을 지우면 그만이지만, 토큰은 이미 발급되어 클라이언트가 쥐고 있으므로 회수할 방법이 마땅치 않다. 이 약점을 다루는 방법이 이 편의 마지막 주제다.


정보가 오래되는 문제도 있다. 토큰에 담긴 정보는 발급 시점의 것이라, 그 뒤에 사용자의 권한이나 상태가 바뀌어도 토큰은 옛 정보를 그대로 물고 다닌다. 세션은 매번 저장소를 조회하므로 최신 상태를 반영하기 쉽지만, 토큰은 담아 둔 정보가 시간이 지나며 실제와 어긋날 수 있다. 그래서 자주 바뀌는 정보는 토큰에 박아 두지 않는 편이 안전하다.

토큰을 어디에 둘 것인가

토큰을 발급받은 클라이언트는 그것을 어딘가에 보관해야 다음 요청에 실어 보낼 수 있다. 이 보관 위치의 선택이 방어의 중요한 갈림길이다. 위치에 따라 노출되는 위협이 다르기 때문이다. 크게는 스크립트가 접근할 수 있는 저장 공간에 두는 방식과, 스크립트가 읽지 못하는 쿠키에 두는 방식이 있다.


스크립트가 접근할 수 있는 저장 공간에 토큰을 두면 다루기는 편하지만, 페이지에 악성 스크립트가 끼어들 경우 그 스크립트가 토큰을 읽어 내갈 수 있다. 반대로 스크립트가 읽지 못하게 막은 쿠키에 두면 이 위협은 줄지만, 쿠키의 자동 전송 성질 때문에 사이트 간 요청 위조에 대비해야 한다. 어느 위치든 고유한 위험을 안고 있다.


그래서 위치를 고를 때는 각 위험을 함께 막을 방어를 갖추었는지를 따져야 한다. 쿠키에 둔다면 다른 출처의 요청에 실리지 않게 제한하고 요청 위조 방어를 겹쳐야 하고, 스크립트가 접근하는 공간에 둔다면 스크립트 주입 자체를 막는 방어에 힘을 실어야 한다. 위치 선택만으로 안전이 결정되지 않으며, 그 위치에 맞는 방어를 함께 세워야 한다.


토큰을 여러 조각으로 나누어, 오래 살아야 하는 값은 스크립트가 읽지 못하는 쿠키에 두고 짧게 쓰는 값만 다루기 쉬운 곳에 두는 절충도 쓰인다. 위험이 큰 값일수록 접근을 좁히고, 위험이 작은 값은 편의를 살리는 것이다. 어느 방식이든 토큰이 새어 나가는 경로를 하나하나 따져 막는 자세가 밑바탕이 된다.

수명과 무효화의 딜레마

토큰의 무효화가 어렵다는 약점은 수명을 짧게 잡는 것으로 크게 완화된다. 토큰이 곧 만료된다면, 새어 나간 토큰이라도 통하는 시간이 짧아 피해가 제한된다. 그래서 토큰 방식에서는 수명을 짧게 두는 것이 기본 방어가 된다. 오래 유효한 토큰은 한 번 새어 나가면 손쓸 방법이 사라지기 때문이다.


그러나 수명을 짧게 하면 사용자가 자주 다시 로그인해야 하는 불편이 생긴다. 이 딜레마를 푸는 것이 짧은 수명의 토큰과 그것을 조용히 다시 발급받는 긴 수명의 수단을 나누어 쓰는 구조다. 실제 요청에는 짧은 토큰을 쓰고, 그것이 만료되면 별도의 수단으로 새 토큰을 받아 이어 간다. 이 구조는 다음다음 편에서 자세히 다룬다.


즉시 무효화가 꼭 필요한 경우에는 서버가 최소한의 상태를 두는 절충을 택하기도 한다. 각 토큰에 고유한 표를 담아 두고, 무효로 만들 토큰의 표를 폐기 목록에 올려 검증 때 대조하는 방식이다. 이렇게 하면 완전한 무상태는 아니지만, 토큰의 이점을 크게 잃지 않으면서 개별 무효화의 손잡이를 얻는다. 상태를 조금 두는 대가로 무효화의 약점을 보완하는 것이다.


결국 토큰 방식의 설계는 상태를 얼마나 둘지의 선택으로 귀결된다. 완전히 상태를 두지 않으면 확장은 쉽지만 무효화가 어렵고, 최소한의 상태를 두면 무효화를 얻는 대신 순수한 무상태의 이점을 조금 내준다. 서비스가 요구하는 무효화의 즉각성과 확장의 필요를 저울질해 그 균형점을 잡아야 한다.

토큰을 다룰 때의 원칙

토큰 방식에서 가장 조심할 지점은 검증이다. 토큰을 만드는 일보다 받아들이는 일이 훨씬 더 신중해야 한다. 서명이 유효한지, 만료되지 않았는지, 발급자가 기대한 대상이 맞는지를 모두 확인한 뒤에야 토큰을 믿어야 한다. 이 가운데 하나라도 빠뜨리면 검증에 구멍이 생긴다.


토큰이 스스로 어떤 방식으로 검증하라고 지시하게 두어서는 안 된다. 검증하는 서버는 자신이 기대하는 방식과 조건을 미리 정해 두고, 그에 맞는 토큰만 받아들여야 한다. 토큰의 지시를 그대로 따르면 그 지시를 악용해 검증을 우회하려는 시도에 노출된다. 검증의 기준은 언제나 서버가 쥐고 있어야 한다.


토큰에는 필요한 최소한의 정보만 담는 것이 좋다. 토큰은 요청마다 실려 다니므로 정보가 많을수록 모든 요청이 무거워지고, 담긴 정보가 오래되어 실제와 어긋날 위험도 커진다. 그래서 토큰에는 사용자를 가리키는 최소한의 정보만 두고, 상세한 내용은 필요할 때 원본에서 조회하는 편이 안전하다.


토큰은 민감한 정보를 담는 그릇이 아니라 신원을 실어 나르는 표라는 관점을 유지하는 것이 중요하다. 내용은 열려 있다고 여기고, 서명은 위조를 막는 봉인이라고 생각하며, 수명은 짧게 두고, 검증은 엄격히 하는 것이 토큰을 안전하게 다루는 기본자세다. 이 원칙들이 지켜질 때 토큰의 이점이 약점을 덮는다.


발급자와 대상을 확인하기

토큰을 검증할 때는 서명과 만료만 보아서는 부족하다. 이 토큰을 누가 발급했고 누구를 위해 발급했는지도 함께 확인해야 한다. 여러 발급자와 여러 서비스가 얽힌 환경에서는, 엉뚱한 곳에서 발급된 토큰이나 다른 서비스를 위해 발급된 토큰이 잘못 받아들여지는 일을 막아야 하기 때문이다.


발급자를 확인한다는 것은 토큰에 적힌 발급 주체가 서버가 신뢰하는 발급자와 일치하는지를 보는 것이다. 서버는 자신이 인정하는 발급자를 미리 정해 두고, 그 발급자가 낸 토큰만 받아들인다. 이렇게 하면 서명 자체는 형식상 올바르더라도 신뢰하지 않는 곳에서 나온 토큰을 걸러 낼 수 있다.


대상을 확인한다는 것은 이 토큰이 바로 이 서비스를 위해 발급된 것인지를 보는 것이다. 하나의 발급자가 여러 서비스를 위한 토큰을 낼 때, 각 토큰에는 어느 서비스를 위한 것인지가 담긴다. 검증하는 서비스는 그 대상이 자신을 가리키는지 확인해, 다른 서비스를 위해 발급된 토큰을 자기 것으로 받아들이지 않도록 한다.


이 두 확인은 검증의 기준을 서버가 쥐고 있어야 한다는 원칙의 연장이다. 서명이 유효한지, 만료되지 않았는지, 신뢰하는 발급자가 냈는지, 이 서비스를 위한 것인지를 모두 통과한 토큰만 믿는다. 확인해야 할 조건을 빠짐없이 갖출수록 예상 밖의 토큰이 비집고 들어올 틈이 줄어든다.

정리하면 토큰 기반 인증은 상태를 서버에 두지 않고 서명된 정보를 요청에 실어 보내는 방식으로, 확장과 분산에 강한 대신 무효화가 어렵다는 약점을 안는다. 토큰의 보관 위치마다 다른 위험이 따르므로 그에 맞는 방어를 함께 세워야 하고, 짧은 수명과 엄격한 검증이 이 방식의 안전을 떠받친다. 다음 편에서는 널리 쓰이는 토큰 형식의 내부 구조를 열어 무엇을 담고 무엇을 담지 말아야 하는지를 들여다본다.