토큰 방식을 이해하고 나면 실제 토큰이 어떻게 생겼는지가 궁금해진다. 널리 쓰이는 형식을 하나 열어 보면 점 두 개로 나뉜 긴 문자열이 나타난다. 얼핏 암호화된 덩어리처럼 보이지만, 각 조각을 풀어 보면 안의 내용이 그대로 읽힌다. 이 사실이 토큰의 서명과 암호화를 헷갈리는 흔한 오해의 출발점이자, 그 오해를 바로잡는 열쇠다.
이번 편은 널리 쓰이는 토큰 형식인 JWT(제이슨 웹 토큰)의 내부 구조를 열어 본다. 세 조각이 각각 무슨 역할을 하는지, 어디에 무엇을 담아야 하고 무엇을 담으면 안 되는지, 서명이 왜 잠금이 아니라 봉인인지, 대칭과 비대칭 서명이 어떻게 갈리는지, 그리고 이 토큰을 다룰 때의 함정이 무엇인지를 짚는다. 구조를 정확히 알아야 이 토큰을 안전하게 발급하고 검증할 수 있다.
세 조각으로 이루어진 토큰
이 토큰은 점으로 구분된 세 조각으로 이루어진다. 앞에서부터 헤더, 페이로드, 서명이며, 각 조각은 사람이 읽는 글자로 바꾸기 쉬운 방식으로 인코딩되어 있다. 점 두 개가 세 부분의 경계를 나누고, 서버는 이 경계를 기준으로 조각을 분리해 해석한다. 하나의 문자열 안에 정보와 그 정보의 진위 증명이 함께 들어 있는 셈이다.
첫 조각인 헤더에는 이 토큰이 어떤 방식으로 서명되었는지가 담긴다. 어떤 서명 알고리즘을 썼는지, 토큰의 종류가 무엇인지 같은 메타 정보가 들어간다. 서버는 검증할 때 이 헤더를 참고해 어떤 방식으로 서명을 확인할지 정한다. 작지만 검증의 방향을 정하는 조각이다.
둘째 조각인 페이로드에는 실제 담고 싶은 내용이 들어간다. 사용자를 가리키는 식별 정보, 발급 시각, 만료 시각 같은 것들이 여기에 담긴다. 이 안에 담기는 각 항목을 클레임, 즉 주장이라고 부른다. 서버가 사용자에 대해 주장하는 사실들을 모아 둔 곳이라고 이해하면 쉽다.
셋째 조각인 서명은 앞의 두 조각이 위조되지 않았음을 보증한다. 서버는 헤더와 페이로드를 자신만 아는 비밀과 함께 계산해 서명을 만든다. 검증할 때 같은 계산을 다시 해서 서명이 일치하는지 보면, 내용이 손대어졌는지 알 수 있다. 이 서명이 있기에 서버는 상태를 저장하지 않고도 토큰을 믿을 수 있다.
여기서 처음 이 토큰을 만나는 쪽이 크게 오해하는 지점이 있다. 세 조각 중 앞의 두 조각은 암호화된 것이 아니라 단지 글자 형태로 바꿔 둔 것이라는 점이다. 그래서 누구든 이 조각을 원래 글자로 되돌려 내용을 읽을 수 있다. 앞의 두 조각은 공개된 것이나 마찬가지이고, 오직 서명만이 위조를 막는 역할을 한다. 따라서 남에게 보이면 안 되는 정보는 이 토큰에 담아서는 안 된다.
담아야 할 것과 담지 말아야 할 것
페이로드에 담는 클레임에는 널리 쓰이는 표준 항목들이 있다. 이 토큰이 누구에 관한 것인지 가리키는 식별 항목, 언제 발급되었는지 나타내는 발급 시각, 언제까지 유효한지 나타내는 만료 시각 등이 대표적이다. 이런 표준 항목들은 검증 로직에서 공통으로 다루기 좋게 이름이 정해져 있어, 먼저 채워 두고 필요한 항목을 덧붙이는 것이 순서다.
만료 시각은 특히 중요한 항목이다. 이 값이 있으면 서버는 토큰이 유효 기간을 넘겼는지 판별할 수 있고, 넘긴 토큰은 거부한다. 만료가 없으면 한 번 발급된 토큰이 영원히 유효해져, 새어 나갔을 때 손쓸 방법이 사라진다. 그래서 만료 시각을 반드시 넣고 그 값을 짧게 잡는 것이 기본이다.
토큰마다 고유한 식별 항목을 넣어 두는 것도 유용하다. 각 토큰에 고유한 번호를 붙여 두면, 나중에 특정 토큰만 골라 폐기 목록에 올리는 관리가 가능해진다. 무상태 토큰의 약점인 개별 무효화를 이 항목이 어느 정도 보완한다. 무효화가 필요한 서비스에서는 이 고유 항목을 빠뜨리지 않아야 한다.
반대로 페이로드에 절대 담지 말아야 할 것이 민감한 정보다. 페이로드는 누구나 읽을 수 있으므로, 비밀번호나 주민 등록 정보 같은 것을 담으면 그대로 노출된다. 토큰에 담기는 항목을 정할 때는 그 값이 공개된 게시판에 붙어도 괜찮은지를 기준으로 삼아야 한다. 그 질문을 통과하지 못하는 값은 토큰에 넣지 않는다.
담는 양도 최소한으로 유지하는 것이 좋다. 토큰은 요청마다 실려 다니므로, 페이로드가 커지면 모든 요청이 무거워진다. 게다가 담긴 정보가 오래되어 실제와 어긋날 위험도 커진다. 그래서 토큰에는 사용자를 가리키는 최소한의 정보만 두고, 상세한 내용은 필요할 때 원본에서 조회하는 편이 안전하다. 특히 권한 정보를 토큰에 박아 두면 권한이 바뀌어도 이미 발급된 토큰은 옛 권한을 그대로 물고 다니므로, 즉시 반영이 중요한 경우에는 권한을 토큰에 담지 않고 서버에서 확인하는 편이 낫다.
잠금이 아니라 봉인인 서명
서명의 역할을 정확히 이해하려면 잠금과 봉인의 차이를 떠올리면 좋다. 잠금은 내용을 감추어 열쇠 없이는 볼 수 없게 하는 것이고, 봉인은 내용은 보이더라도 누군가 손댔는지를 알 수 있게 하는 것이다. 이 토큰의 서명은 잠금이 아니라 봉인에 해당한다. 내용은 그대로 보이지만, 봉인이 깨졌는지로 위조 여부를 판단한다.
봉인의 원리는 서버만 아는 비밀에 있다. 서버는 헤더와 페이로드를 그 비밀과 함께 계산해 서명을 만들고, 검증할 때 같은 계산을 반복한다. 비밀을 모르는 쪽은 내용을 바꾼 뒤 올바른 서명을 다시 만들어 낼 수 없다. 내용을 조금이라도 바꾸면 서명이 어긋나 봉인이 깨진 것이 드러난다.
그래서 서명이 지키는 것은 무결성이지 비밀이 아니다. 무결성은 내용이 바뀌지 않았음을 보증하는 성질이고, 비밀은 내용을 감추는 성질이다. 이 토큰은 무결성만 제공하고 비밀은 제공하지 않는다. 이 구분을 놓치면 민감한 정보를 서명만 믿고 담는 실수를 저지르게 된다.
봉인의 신뢰는 전적으로 비밀의 보호에 달려 있다. 서버의 비밀이 새어 나가면 남이 스스로 유효한 봉인을 찍은 토큰을 만들 수 있게 되고, 그러면 서버는 그 위조 토큰을 진짜로 받아들인다. 그래서 이 비밀은 코드에 적지 않고 안전한 저장소에 두며, 유출이 의심되면 즉시 교체할 수 있도록 준비해 두어야 한다. 여러 비밀을 함께 두고 검증 시 여러 개를 시도할 수 있게 해 두면, 옛 토큰을 무효화하지 않고도 비밀을 바꿔 나갈 수 있어 정기 교체가 수월해진다.
대칭 서명과 비대칭 서명
서명을 만드는 방식은 크게 두 갈래로 나뉜다. 하나는 하나의 비밀을 서명과 검증에 함께 쓰는 대칭 방식이고, 다른 하나는 서명용 비밀과 검증용 공개 값을 나누는 비대칭 방식이다. 이 둘은 쓰임새가 서로 달라, 토큰을 누가 만들고 누가 검증하느냐에 따라 골라야 한다.
대칭 방식은 하나의 비밀을 발급자와 검증자가 공유한다. 구조가 단순하고 계산이 빠르며, 발급과 검증을 같은 서버가 하는 경우에 잘 맞는다. 대신 검증하는 쪽도 비밀을 알아야 하므로, 검증자가 여럿이면 그 비밀이 여러 곳에 퍼져 위험이 커진다. 발급과 검증이 한 울타리 안에서 이루어질 때 어울리는 방식이다.
비대칭 방식은 서명용 비밀과 검증용 공개 값을 분리한다. 발급자는 비밀로 서명하고, 검증자는 공개 값으로 검증한다. 공개 값은 이름 그대로 널리 알려져도 안전하므로, 검증자가 여럿이어도 비밀이 퍼질 위험이 없다. 여러 서비스가 한 발급자의 토큰을 각자 검증해야 하는 구조에서 이 방식이 빛난다.
이 차이는 신뢰의 범위를 어떻게 나누느냐의 문제다. 대칭 방식은 발급자와 검증자가 같은 비밀을 믿는 하나의 신뢰 영역을 이루고, 비대칭 방식은 발급 권한을 한곳에 모으면서 검증은 널리 퍼뜨릴 수 있다. 처음에는 단순한 대칭 방식으로 시작하고, 서비스가 여러 갈래로 나뉘면서 비대칭 방식으로 옮겨 가는 흐름이 자연스럽다. 어느 방식이든 검증하는 서버는 자신이 기대하는 알고리즘을 미리 정해 두고 그 방식으로만 검증해야 한다.
토큰을 다룰 때의 함정들
이 토큰에는 잘 알려진 함정이 몇 가지 있고, 그중 가장 위험한 것이 알고리즘을 우회하는 문제다. 헤더에 서명 방식을 없음으로 지정해 서명 없는 토큰을 보내는 수법이 대표적이다. 검증 로직이 헤더의 지시를 그대로 따르면 서명을 확인하지 않고 통과시킨다. 검증할 때 허용할 알고리즘을 코드에서 고정해, 없음을 비롯한 예상 밖의 방식은 처음부터 거부해야 한다.
두 번째 함정은 만료 시각을 검증하지 않는 것이다. 만료 항목을 넣어 두고도 검증 단계에서 그 값을 확인하지 않으면, 만료된 토큰이 계속 통과한다. 이러면 수명을 짧게 잡은 의미가 사라진다. 검증 로직에서 서명 확인과 함께 만료 확인을 반드시 넣어, 기한이 지난 토큰은 자동으로 걸러 내야 한다.
세 번째 함정은 앞서 강조한, 민감한 정보를 페이로드에 담는 것이다. 서명만 믿고 페이로드에 비밀을 담으면, 그 내용은 봉인은 되어 있으나 잠기지는 않은 채로 노출된다. 발급 코드에서 페이로드에 담기는 모든 항목이 공개되어도 무방한지를 다시 점검해, 담아서는 안 될 값이 슬그머니 들어가는 것을 막아야 한다.
비대칭 방식에서는 알고리즘을 뒤섞는 혼동 문제도 조심해야 한다. 공개 값을 대칭 방식의 비밀인 것처럼 악용해 유효해 보이는 토큰이 만들어지는 상황인데, 이 역시 검증 시 알고리즘을 고정하고 방식을 뒤섞지 않으면 막을 수 있다. 이 함정들은 대부분 검증을 느슨하게 할 때 열리므로, 토큰을 만드는 것보다 검증하는 쪽이 훨씬 더 조심스러워야 한다. 통과 조건을 좁게 잡을수록 예상 밖의 토큰이 비집고 들어올 틈이 줄어든다.
발급 시점에 정하는 것들
토큰의 안전은 검증에서만 결정되지 않는다. 발급 시점에 무엇을 어떻게 담느냐가 그 토큰의 성질을 미리 정한다. 발급 코드는 페이로드에 담을 항목과 그 값의 수명을 정하고, 어느 방식으로 서명할지를 고른다. 이 결정들이 검증 단계에서 확인할 것들과 짝을 이룬다.
발급 시점에는 만료 시각을 반드시 넣고 그 값을 짧게 잡는다. 발급 시각을 함께 담아 두면 토큰이 언제 만들어졌는지를 추적할 수 있고, 유효가 시작되는 시점을 따로 지정해 아직 쓰여서는 안 되는 토큰을 미리 발급해 두는 일도 가능해진다. 이런 시각 항목들이 토큰의 유효 구간을 명확히 그어 준다.
발급자와 대상을 담는 것도 발급 시점의 몫이다. 이 토큰을 누가 냈고 어느 서비스를 위한 것인지를 담아 두면, 검증하는 쪽이 그 값을 대조해 엉뚱한 토큰을 걸러 낼 수 있다. 발급 시점에 이 정보를 성실히 채워 두어야 검증 시점의 확인이 의미를 가진다. 발급과 검증은 이렇게 서로를 전제한다.
발급 시점에 담지 말아야 할 것을 거르는 일도 중요하다. 페이로드에 슬그머니 민감한 값이 들어가지 않도록, 발급 코드에서 담기는 항목을 한 번 점검하는 절차를 두는 것이 안전하다. 담을 것을 성실히 채우고 담지 말아야 할 것을 걸러 내는 두 가지가 발급 단계에서 함께 이루어져야, 그 토큰이 검증을 거쳐 신뢰될 자격을 갖춘다. 발급과 검증을 한 사람이 짜더라도 이 둘을 별개의 관문으로 여기고 각각의 조건을 빠짐없이 챙기는 태도가, 구조가 단순해 보이는 이 토큰을 안전하게 지키는 밑바탕이 된다.
정리하면 이 토큰은 헤더와 페이로드와 서명이라는 세 조각으로 이루어지며, 앞의 두 조각은 감춰지지 않고 서명만이 위조를 막는 봉인 역할을 한다. 페이로드에는 표준 항목과 최소한의 정보만 담고 민감한 값은 넣지 않으며, 서명 비밀은 철저히 보호해야 한다. 검증할 때는 알고리즘을 고정하고 만료를 반드시 확인해 흔한 함정을 피해야 한다. 다음 편에서는 짧은 수명의 토큰과 긴 수명의 갱신 수단을 함께 쓰는 구조를 자세히 들여다본다.