이번 포스팅에서는 세션과 JWT를 이용한 인증 방식에 대해서 알아보겠습니다.
세션과 JWT를 다루기 전에 먼저 알고 넘어가야하는 중요한 개념이 있는데요, 바로 인증과 인가입니다.

인증과 인가

인증

인증(Authentication)은 쉽게 말해서 로그인 이라고 할 수 있습니다.
클라이언트가 이 사이트에 가입된 회원임이 맞는지 아이디와 패스워드 등을 통해서 말 그대로 인증 받는 것이지요.
예를들어, Github에 이메일과 패스워드를 통해서 로그인을 했다고 가정해본다면 Github 서버는 사용자가 입력한 로그인 정보를 통해서 Github에 가입된 회원이 맞는지 검증하고 확인한 것입니다.
위와 같이 클라이언트가 이 사이트에 가입된 회원이 맞는지를 검증하는 과정인증 이라고 할 수 있습니다.

인가

인가(Authorization) 로그인이 된 사용자. 즉, 인증이 된 사용자가 서비스를 이용하면서 특정 자원에 접근이 가능한지를 확인 하는 과정 입니다.
Github 로그인을 통해 인증이 되었다면 나의 리포지토리 설정 페이지 처럼 로그인을 해야만 접근가능한 페이지에 접근을 할 수 있게 됩니다.
Github가 내가 로그인되어 있음을 확인하고 이 페이지에 접근 가능하도록 인가를 해주었기 때문에 접근이 가능한 것 입니다.


이제 우리는 인증인가가 무엇인지 알게되었습니다.
그렇다면 로그인을 통해 인증을 한 사용자를 식별하기 위해서 사용자의 로그인 정보는 어디에 저장을 해야할까요?
로그인 정보를 어디서 보관하는지에 따라 세션 인증 방식과 토큰 인증 방식으로 나눌 수 있습니다.



세션 인증 방식과 토큰 인증 방식

세션 인증 방식

1

위 사진을 간략하게 정리하자면 다음과 같습니다.

  1. 사용자가 로그인에 성공하면 서버는 세션정보를 생성합니다.
  2. 세션정보는 서버 내 세션 저장소에 보관되고 세션ID를 발급하여 사용자 브라우저에 응답하게 됩니다.

    세션 저장소는 서버 내의 메모리일 수도 있고 데이터베이스일 수도 있습니다.
    하지만 데이터베이스에 인증정보를 저장하는 것은 매 요청마다 데이터베이스 연결해야하는 치명적인 단점이 있기 때문에 주로 접근이 빠른 메모리에 저장하게 됩니다.

  3. 사용자 브라우저는 세션ID란 이름의 쿠키로 저장되고 앞으로 모든 요청에 세션ID를 담아 서버로 요청을 보냅니다.
  4. 브라우저가 요청을하면 서버는 요청의 세션ID를 확인해서 세션저장소에서 세션ID와 맞는 유저정보를 찾아 확인하고 인가를 해줍니다.

이처럼 서버에 로그인 되어있음이 지속되는 이 상태세션 이라고 합니다.

하지만 이 세션 방식은 조금만 생각해봐도 허점이 있다는 것을 알 수 있습니다.

대표적인 세션 인증 방식의 문제점

1.사용자가 동시에 서버에 많이 접속할 경우

2

첫 번째로 사용자가 동시에 많이 접속한다면 세션저장소 즉 메모리에 많은 세션정보가 저장되게되고 서버는 많은 부하를 받게 됩니다.
또한 많은 부하로 인해 서버가 꺼져버리기라도 한다면, 즉 서버가 재부팅 되어야 하는 상황이 오면 메모리에 저장된 휘발성 세션정보가 다 손실 되어 사용자는 재 로그인을 하게 되어야 합니다.

2. 서버를 여러대두고 운영한다면

두 번째로 여러대의 서버두고 운영한다면 각각의 서버가 세션저장소를 가지고 있을 것이고 요청이 들어올 때 로드밸런서를 통해 여러 서버들 사이에 요청을 분산될 것 입니다.
만약 사용자A가 로그인은 1번 서버에서 했고 로그인 이후 기능의 요청은 3번 서버로 간다면 3번 세션저장소에는 사용자A의 세션정보가 없기 때문에 세션유지가 되지 않습니다.
그렇다고 사용자 요청이 각자 할당된 서버로만 보내지게 하는 것도 굉장히 번거롭고 까다로운 작업입니다.
서버가 복잡한 구성과 환경속에서 어떤 상태를 기억해야한다는 것이 이 세션 방식의 두번째 문제입니다.

그래서 이러한 세션방식의 문제점을 해결하고자 고안된 방식이 토큰 인증 방식인 JWT 입니다.


토큰 인증 방식 : JWT

4

세션 인증 방식은 서버에 인증정보를 저장하는 방식이라면, 토큰 인증 방식은 인증정보를 클라이언트가 직접 보관하는 방식입니다.
여기서 클라이언트가 직접 보관하고있는 인증정보를 JWT라고 합니다.

JWTJson Web Token의 약자로 웹에서 사용되는 토큰 형식의 전자 서명된 문자열을 의미하며 JWT를 자세히 살펴보면 다음과 같이 Base64로 인코딩 된 문자열 형태로 구성되어 있는 것을 확인할 수 있습니다.

그럼 이 알 수 없는 인코딩 문자열을 무엇을 의미하는 것 일까요?

위 사진에서 인코딩된 문자열이 빨간색 . 보라색 . 하늘색 총 세 부분으로 이루어져 있는 것을 확인할 수 있습니다.

6

이 암호화 된 문자열을 디코딩 해보면 세 부분으로 나뉘는데 각각 헤더, 페이로드, 시그니처로 구분됩니다.

제일 먼저 두 번째 구성요소인 페이로드를 살펴보겠습니다.
페이로드에는 누가 누구에게 발급했는지, 이 토큰이 언제까지 유효한지, 그리고 서비스가 사용자에게 이 토큰을 통해 공개하기 원하는 내용. 이를테면 사용자의 닉네임이나 서비스상의 레벨, 관리자 여부 등을 서비스 측이 원하는대로 담을 수 있습니다.
이렇게 토큰에 담긴 사용자 정보 등의 데이터를 Claim이라고 합니다.

Claim은 특별한 암호화도 아니고 Base64로 인코딩 되어 있기 때문에 사용자가 자바스크립트나 복호화 사이트에서 손쉽게 디코딩해서 내부를 알 수 있는데요, 사용자가 예를들어 토큰의 유효기간을 임의로 늘린다던지, 관리자 여부를 true로 바꿔 관리자 권한으로 인가를 받는다던지 등 이 Claim 내용들을 조작해서 악용할 수 있다는 생각이 드실 수 있다고 생각됩니다.

이 것들을 막기위해 첫 번째 구성요소인 헤더와 세 번째 구성요소인 시그니처가 존재합니다.

7

첫 번째 구성요소인 헤더를 디코딩하면 두 가지 정보가 담겨있습니다.
먼저 타입. 토큰의 타입인데 여기에는 언제나 고정값으로 JWT가 들어갑니다. 타입이 JWT여야 JWT인것이죠.
다른 하나는 alg, 알고리즘의 약자인데 여기에는 세 번째 구성요소인 시그니처를 만드는데 사용되는 알고리즘(HS256, HS512등 여러 암호화 방식)이 지정됩니다.

첫 번째 구성요소인 헤더와 두 번째 구성요소인 페이로드 그리고 서버만 알고있는 비밀키 이 셋을 이 암호화 알고리즘에 넣고 암호화 시키면 세 번째 구성요소인 시그니처가 나오게 되는 것 입니다.

암호화는 인코딩만 가능하고 디코딩은 불가한 단방향으로 이루어지기 때문에 클라이언트에서 토큰 정보를 탈취하더라도 서버만 알고있는 비밀 키를 찾아 낼 방법이 없습니다.
또한 페이로드의 문자가 단 하나만 수정되어도 시그니처 값이 완전히 달라지며 만약 페이로드를 수정되었다고 해도 유효한 시그니처가 나올라면 서버만 알고있는 비밀 키를 알고 있어야 되기 때문에 조작 자체가 불가능한 것이지요.

실제로 서버는 JWT가 담겨 요청이온 정보를 확인할 때 헤더와 페이로드 값을 서버의 비밀키로 돌려봐서 계산된 결과값이 시그니처 값과 일치하는 결과가 나오는지 확인합니다.
만약 페이로드 정보가 서버가 아닌 누군가에 의해 조작되었다면 요청을 거부합니다.
시그니처와 계산값이 일치하고 유효기간도 지나지 않았다면 그 사용자는 로그인 된 회원으로서 인가를 받는겁니다.


세션의 상태유지와 토큰의 무상태 프로토콜

토큰인증방식은 서버는 사용자들의 상태를 따로 저장할 필요 없이 비밀키만 가지고 있으면 요청이 들어올 때 마다 토큰을 확인해서 요청을 걸러낼 수 있습니다.

이처럼 시간에 따라 바뀌는 어떤 상태값을 갖지 않는 것stateless 무상태라고 하고,
서버에서 상태값을 갖는 세션은 반대로 stateful 이라고 합니다.

그렇다면 항상 세션보다 JWT가 우선적일까요? 안타깝게도, 세션을 대체하기에는 JWT에 큰 결점이 있습니다.

세션처럼 stateful해서 모든 사용자의 상태를 기억하고 있다는 건 구현되기 부담되고 고려사항도 많지만 서버에서 클라이언트들의 상태를 언제든 제어할 수 있다는 의미로 해석할 수 있습니다.

예를들어 사용자A를 강제 로그아웃 시키려면 세션저장소에서 사용자의 세션 ID를 삭제 시켜 기존 세션을 없애고 다음 요청부터 세션정보가 없어 사용자A에 대한 요청이 거부됩니다.

하지만 JWT는 인증 정보를 클라이언트가 가지고 있고 서버는 사용자의 상태정보를 전혀 저장하고 있지 않기 때문에 이미 발급된 토큰정보를 제어할 수 없습니다.

최악의 상황으로 만료기간이 살아있는 JWT를 해커에게 탈취 당하더라도 탈취 당한 토큰을 무효화할 수 있는 방법이 없다는 것 입니다.


Access Token과 Refresh Token

이런 점을 나름대로 보완하기 위해서 보통 엑세스 토큰의 만료시간을 짧게 설정합니다.
지금까지 설명 드렸던 토큰을 엑세스 토큰이라고 생각하시면 됩니다.

예를 들어 엑세스 토큰의 만료시간을 10분만 설정해둔다고 가정해보겠습니다.
토큰의 만료시간을 10분으로 짧게 설정했기 때문에 금방 로그인이 만료되며 해커가 토큰이 탈취하더라도 서비스를 이용할 수 없게 될 것 입니다.

그렇다면 일반 사용자는 토큰 정보가 탈취되는 문제점 때문에 매번 10분 마다 재 로그인을 해야되는게 아니냐는 생각이 드실텐데요.
그래서 로그인을 하면 만료기간이 짧은(보통 한시간 내) 엑세스 토큰과 만료기간이 상대적으로 긴(보통 2주이하) 리플래시 토큰을 함께 발급합니다.

이 둘을 구현하는 방법은 여러 방법들이 있지만, 실제로 E2E 프로젝트 때 제가 소속했었던 Anabada 팀에서 구현한 방법을 소개 해드리겠습니다.

10

  1. 클라이언트에서 로그인 요청을 보내면 서버에서 엑세스 토큰리플래시 토큰을 발급. 레디스에 리플래시 토큰의 정보를 저장 후 사용자에게 엑세스 토큰리플래시 토큰을 반환합니다.
  2. 이후 매 요청마다 엑세스 토큰을 HTTP 요청정보에 담아보내고 서버는 이 엑세스 토큰 검증을 통해 인가를 주어 로그인 상태로 서비스를 이용할 수 있도록 합니다.
  3. 서비스를 이용하다가 시간이 흘러 엑세스 토큰이 만료되고 만료된 엑세스 토큰을 서버에 요청하면 서버는 토큰이 만료되었다는 응답을 반환합니다.
  4. 클라이언트는 엑세스 토큰이 만료되었다는 응답을 보고 리플래시 토큰을 서버에 전송합니다.
  5. 서버는 요청 받은 리플래시 토큰과 레디스에서의 해당 사용자의 리플래시 토큰 정보를 비교하여 만료기한이 유효한지, 해당 사용자의 리플래시 토큰인지 확인한 후 엑세스 토큰재발급하여 사용자에게 반환합니다.
  6. 만약 리플래시 토큰도 만료되었다면 로그인창으로 이동시킵니다.

레디스에 리플래시 토큰 정보를 저장한다는 것이 서버에서 상태정보를 저장하는 세션방식과 동일하지 않냐라는 생각을 하실 수 있는데요.
먼저 서버에서 요청이 올때마다 세션저장소에서 인증정보를 확인하는 세션 방식과 달리 엑세스 토큰이 만료되었을 때만 레디스를 확인하여 최대한 네트워크 비용을 줄였습니다.
또한 JWT를 사용하되, 서버에서 사용자의 인증정보를 최소한으로 저장하여 클라이언트를 제어할 수 있는 방법을 고안했습니다.


RTR 전략

마지막으로 RTR은 리플래시 토큰마저 탈취당할 수 있다는 것을 고려한 방식으로 엑세스 토큰을 재발급할 때 리플래시 토큰도 같이 갱신 시켜버리는 방식 입니다.
따라서 엑세스 토큰을 1회만 재발급할 수 있습니다.

RTR의 핵심은 리플래시 토큰을 1회용으로 구현하는 것입니다.


마무리

이번 주제에서 세션에 문제점 때문에 고안된 JWT에 대해 알아봤지만 JWT도 극복할 수 없는 단점이 있다는 것을 확인할 수 있었습니다.
세션과 토큰 인증 방식중 ‘ 이 방식이 제일 보안이 훌륭해 ! ‘ 라고는 할 수 없습니다.

세상에 완벽한 보안은 없고, 그것이 여전히 해킹이 발생하고 있는 이유입니다.

JWT가 세상에 나온 것 처럼 앞으로도 많은 보안이슈를 해결하기 위한 방법들이 나올 것 이고 그 방법들을 생각하는건 개발자가 해야할 몫 이라고 생각합니다.

현재 시스템의 환경과 상황을 고려해서 제일 알맞은 보안 방법을 찾아야하고 가장 적합한 보안 방식을 선택하여 적용하는 것이 가장 중요할 것 입니다.