로그인 JWT로 만들었어, JWT가 유행이잖아

insukL·2024년 3월 8일
45
post-thumbnail

서론

토이 프로젝트를 진행하면서 흔히 로그인 인증 과정을 만들게 되면 자연스레 JWT로 만들어야지라는 이야기를 듣는다. 그런데 막상 JWT를 쓰기 시작하면 왜? 라는 생각이 꼬리를 문다.

프로젝트로 회원 부분을 구현하면서 JWT에 대해서 많이 되짚어보게 되었고, 당시에 개념을 확립하기 위해서 많이 노력했다.

나만 그런 줄 알았는데, 주변에서 처음 로그인을 구현한다고 하면 'JWT 잘 앎?'이라는 질문이 날아온다. 그리고 JWT의 기본적인 특징을 몰라서 '어떻게 해야하죠?' 라는 식의 대화가 나오는 경우도 잦았다. 그래서 한 번 정리해두면 좋을 것 같아 글을 작성했다.

HTTP의 무상태성 (Stateless)

우선 거슬러 올라가 HTTP의 무상태성을 알 필요가 있다.

Stateless란?

HTTP는 서버-클라이언트 구조를 가진다. 그래서 클라이언트에서 서버에 요청을 보내고, 응답을 받는 구조로 진행된다.

이 때, 서버는 클라이언트에 대한 정보를 저장하지 않고 완료된 요청을 기억하지 않는다. 다시 말하면 클라이언트가 요청을 보내서 응답을 받고, 다시 요청을 보내면 이전 요청과 독립적인 관계가 된다.

그래서 Stateless 상태로 통신을 진행하면 클라이언트는 매번 이전 요청에 대한 정보를 추가로 덧붙여 보내야 한다. ( ex) 장바구니에 상품을 담아둬도, 결제 시엔 장바구니 리스트를 보내야 함)

왜 Stateless인가

이전 요청을 기억하지 않아서 클라이언트가 계속 추가적으로 데이터를 붙여야 하는데, 왜 이런 구조를 사용하는 걸까?

Stateless는 서버가 클라이언트에 대한 정보를 저장하지 않기 때문에, 서버 설계가 간단하다. 그리고 어떤 서버에 요청해도 동일하게 요청을 처리한다는 특징을 가진다. 이런 특징은 분산 서버 환경에서 어떤 서버에 요청을 해도 동일한 처리가 진행한다는 이야기이고, 곧 Scale-out에 대한 이점으로 이어진다. 뿐만 아니라 요청에 실패하거나 장애가 발생해도 금방 대응이 가능하다는 이점이 생긴다.

간단히 정리하면 다음과 같다

  1. 서버와 클라이언트가 느슨하게 결합된다
  2. 서버의 설계가 간단하다
  3. 서버 규모 확장/축소가 쉬움 (Scale-out)
  4. 충돌 후에도 쉽게 다시 시작이 가능하다

쿠키(Cookie)

그래서 HTTP가 Stateless를 하는 이유를 알겠는데, 서비스를 만들다 보면 상태값을 저장해야 하는 경우가 있다. 대표적으로 회원 기반 서비스를 떠올릴 수 있다. 회원 정보를 바탕으로 서비스를 제공해야 하는 경우, 요청이 어떤 회원이 보냈는지 알 수 있어야 한다.

이렇게 추가적으로 필요한 정보가 필요한 경우 클라이언트 측 브라우저에 정보를 저장해둔다. 그리고 요청에 포함 시켜 보낼 수 있도록 만든 것이 쿠키(Cookie)다. 클라이언트가 데이터에 대한 부하를 지기 때문에 서버 측에선 적은 부하로 많은 데이터를 유지하는 방법으로 사용할 수 있다.

그래서 흔히 여러 사이트를 돌아다니면 쿠키 수집에 동의를 구한다. 이는 쿠키를 통해서 사용자의 추가 정보를 요청에 보낼 수 있게 하기 위함이다.

하지만 쿠키는 사용자가 요청에 넣어주는 구조를 가지고 있기 때문에 요청의 길이가 길어지고, 그에 따른 크기에 제약이 있다. 뿐만 아니라 삭제하기 너무 쉽고 제일 큰 문제는 위변조하거나 정보를 탈취 당하기 쉽다는 문제가 있다.

세션(Session)

Session은 쿠키의 문제점을 서버측에서 해결해보고자 구현한 기술이다.

  1. 세션 id를 생성해서 서버는 세션 id에 매핑해서 추가적인 정보를 저장해둔다
  2. 서버는 클라이언트에게 세션 id를 전달한다
  3. 클라이언트는 서비스 요청 시 세션 id를 쿠키로 같이 전달한다
  4. 서버는 세션 저장소에서 세션 id로 검색해서 정보를 사용한다

세션은 중요 정보를 서버측에 저장해두기 때문에 탈취나 위변조에 대한 걱정이 없다. 하지만 서버에 다시 데이터에 대한 부하를 가지게 되는 문제가 있다.

뿐만 아니라 분산 환경에서 문제가 발생할 수 있는데, 각 서버는 세션 저장소를 공유하지 않는다는 점이다. 예를 들어 A 서버에서 인증을 진행한 유저가 다음 요청을 했을 때, 로드 밸런싱이 B 서버로 이어지면 동일한 서비스지만 인증 정보가 유지되지 못하기 때문에 새로 인증 요청이 진행된다. 이런 문제는 별도의 Sticky Session으로 해결할 수 있다.

Sticky Session

Sticky Session은 단어처럼 특정 세션을 특정 서버에 챡 하고 붙이는 개념이다. 앞선 예시에서 A 서버에서 인증한 후 B 서버에 인증 정보가 없어서 문제가 되니, 해당 세션의 요청을 전부 A 서버에만 보내는 방식을 사용한다.

하지만 머릿속에서 한 번 그려보면 Sticky Session의 문제가 보인다. 예를 들어, 특정 서버에서 로그인 작업이 빈번히 진행되었다면? 모든 서비스가 해당 서버에 집중될 것이다. 그러면 결국 로드 밸런싱을 사용하는 의미가 없이 서버가 부하를 감당하지 못할 것이다.

Sticky Session의 단점
1. 로드 밸런싱이 제대로 되지 않아 특정 서버에 과부하가 올 수 있다
2. 특정 서버가 실패하면 서버에 있는 세션 정보가 모두 소실된다

단점을 보완하기 위해 서버를 클러스터화하거나 세션 서버를 따로 두는 방법이 있다.

JWT (Json Web Token)

Session의 이야기를 들어보면 알겠지만, Session 자체는 쿠키를 활용한 Stateful한 방식이다. 매 요청마다 세션 id를 저장소에서 검색하는 과정을 거쳐야 하고, 많은 요청에 대해서 부하가 발생한다. 그리고 Stateful한 방식은 HTTP의 장점이었던 Scale-out에 대한 이점을 잘 살릴 수 없는 방법이다.

그래서 JWT가 등장했고, 큰 인기를 끌었다. JWT는 뜻 그대로 JSON 형태의 웹에서 사용하는 토큰이다. 서비스에 로그인 적혀 있으면 JWT가 보이고, 사이드 프로젝트를, 토이 프로젝트를 열어보면 JWT로 가득하다.

왜 이렇게 인기를 끌게 되었는지 JWT를 좀 더 자세히 살펴보자.

구조

우선 JWT의 구조에 대해 살펴보자. JWT는 헤더, 페이로드, 시그니처라는 3부분으로 구분되어 있다. 그리고 각 부분은 Base64로 인코딩해서 .으로 구분한다. 다시 말해서, 전체 구조는 헤더.페이로드.시그니처로 구성되고, 각각 Base64로 인코딩되어 있다.

시그니처 생성을 위해 어떤 알고리즘을 사용할지, 이 토큰은 어떤 토큰인지 기록하는 부분으로 alg와 typ으로 이뤄진다.

typ은 JWT로 사용하기 때문에 JWT로 사용한다.
alg는 어떤 알고리즘을 가지고 시그니처 부분을 암호화할지 정한다.

{
  alg : "HS256",
  typ : "JWT"
}

Payload

실질적인 토큰의 정보가 저장되어 있는 부분이다. 이렇게 저장되어 있는 정보의 각 부분을 클레임(Claim)이라고 한다.

iss(토큰 발급자), exp(만료 시각)과 같은 표준 클레임도 있고 클라이언트와 서버 간에 임의로 지정한 정보를 넣는 비공개 클레임도 존재한다.

{
  id : 1,
  exp : 1480849147370
}

Signature

이 토큰이 올바른 토큰인지 확인하는 부분으로 시그니처가 없다면 단순히 문자열에 불과하다. 시그니처는 헤더와 페이로드를 Base64로 인코딩한 뒤 .으로 연결한다. 인코딩한 값을 비밀 키를 사용해서 암호화를 진행하고 암호화된 값을 다시 Base64로 인코딩한다.

왜 JWT인가?

JWT에서 Signature를 보면 비밀키로 암호화한 값임을 알 수 있다. 그리고 시그니처는 헤더와 페이로드를 암호화한 값이다.

그렇다. 우리는 이미 있는 값에 비밀키만 알고 있으면 해싱을 진행해서 시그니처와 비교할 수 있다. 비밀키를 통해 이 토큰이 우리가 발급한 올바른 토큰인지 확인할 수 있다.

이 특징을 통해서 세션과 다른 강점을 가진다.

  1. 저장소를 거치지 않아도, 비밀키만 있으면 인증을 진행할 수 있다
  2. 분산 환경에서도 비밀키만 공유된다면 인증을 진행할 수 있다

1번 강점을 통해서 서버는 저장소에 갔다 오는 부하를 줄일 수 있다. 그리고 2번 강점은 특히 기존의 세션 방식에 비해 분산 환경에서 충분히 활용 할 수 있다는 장점이 있다. 그래서 MSA 환경이 급부상하면서 분산 환경에서 유용하게 쓸 수 있는 JWT의 인기가 많아졌다.

JWT를 사용하면서 생각해야 할 부분

열렬한 인기에도 불구하고, JWT에 대해 명확한 개념이 없는 채로 유행하니까~ 블로그에 레퍼런스가 많으니까~ 라는 이유로 사용하는 경우가 잦다. 그러면 이미 JWT에서 보증하는데 추가적인 공수가 들어가거나 잘못 사용하는 경우가 생긴다.

Payload 읽기 문제

처음 JWT를 접하면서 많이 하는 실수는 Payload에 크리티컬한 정보를 저장하는 것이다. 보통 인증을 위한 토큰이라고 이야기하고, 눈에는 문자의 나열로만 보이니 정확한 개념이 없으면 발생하는 문제다.

하지만 설명에 있듯이 Signature를 제외하면 Header와 Payload는 Base64로 인코딩되어 있기 때문에 단순히 디코딩을 통해서 해당 내용을 읽을 수 있다. 다시 말해서 무슨 정보를 저장하든 토큰을 읽을 수만 있으면 어떤 정보가 담겨 있는지 알 수 있다. 절대 노출되면 안되는 정보는 JWT에 담지 말자

만료 시간 예측 문제

Payload에 노출되면 안되는 정보가 담기는 것과 반대로 JWT를 인증을 위한 신성한 토큰을 생각하면 발생할 수 있는 문제다.

예를 들어, JWT의 내부를 고려하지 않고 JWT의 만료 여부를 판단한다고 생각해보자. 생각할 수 있는 방법은 흔히 2가지 정도가 나올 것이다.

  1. JWT를 서버에 한 번 전송해서 에러 메시지가 반환되는지 확인한다
  2. 유효 기간 정보를 미리 알고, JWT를 발급 받는 시점에서 만료 시간을 저장해두고 비교한다

우선 1번 방법은 불필요한 네트워크 통신을 발생 시킨다. 예를 들어, 사이트 재접속 시 기존 토큰이 만료되었는지 파악이 필요한 경우가 있다. 그래서 아무 API를 호출하는 경우가 있는데, 이러면 불필요한 네트워크 통신이 있을 뿐만 아니라 의미 없는 서버 연산이 동반될 수 있다.

2번 방법은 클라이언트에서 불필요한 부하를 지게 된다. 유효 기간 정보를 미리 알고 있어야 하는 부분이나 각 토큰의 만료 시점을 추가로 저장해야 하는 문제가 있다.

Payload는 Base64로 인코딩 되어 있기 때문에 어떤 클라이언트에서도 Base64로 디코딩하면 Payload의 내용을 읽을 수 있다. 그리고 Payload에 흔히 exp(만료시간)이 저장되어 있기 때문에 해당 토큰이 만료된 토큰인지 유효한 토큰인지 파악할 수 있다.

Payload를 읽음으로 클라이언트는 JWT 내부를 확인하고, 남은 시간에 따라 적절히 토큰을 재요청할 수 있다. 개념에 대해 서버와 클라이언트 양측의 적절한 이해가 필요하다.

JWT 탈취

JWT의 가장 큰 문제는 탈취의 위협이다. JWT로 인증을 진행하면 아무 값도 매핑하지 않기 때문에 저장소에서 데이터를 찾지 않는다. 다시 말해, 토큰이 탈취되면 서버는 아무런 값을 매핑하고 있지 않으므로 만료되기 전까지 아무런 대응을 할 수 없다.

Refresh Token

그럼 탈취에 대해 대비하기 위한 방법을 떠올려 보자. 첫 번째로 토큰에 대해 서버에 저장해두고 매번 확인하는 방법. 이 방법은 JWT가 가진 Stateless한 장점이 사라지는 문제가 있다. 두 번째 방법은 짧은 유효 기간을 탈취되어도 금방 만료되게 하는 방법. 이 방법은 사용자는 자주 로그인 요청을 받게 되므로 UX에 악영향을 끼칠 수 밖에 없다.

이에 두 해결책을 적절히 섞어 각각의 단점을 해결하기 위해 Refresh Token이란 방법이 있다.

서버는 클라이언트에게 2가지 토큰(Access Token, Refresh Token)을 발급한다.

  • Access Token : 실제 서비스 내 인증에 사용되는 토큰
  • Refresh Token : Access Token을 재발급 받기 위한 토큰

각 서비스 별로 정책이 다르지만 Access Token은 짧은 유효 기간을 가진다. 탈취 당하더라도 금방 만료되어 악용할 여지를 최대한 줄인다.

이에 반해 Refresh Token은 비교적 긴 시간의 토큰을 생성한다. 여기서 Refresh Token만 서버에 저장한다. Refresh Token에 대해서 서버는 블랙 리스트, 화이트 리스트를 작성하거나 토큰과 정보의 매핑을 삭제 및 수정하는 것으로 Refresh Token에 대해 최소한의 제어권을 가진다.

최소한의 제어권이라는 이야기는 주변에 Refresh Token을 DB에 저장하는 이유에 대해 설명하면서 나왔다. 만약 Access Token의 탈취를 보완하기 위해 토큰을 분리한 점만 있다면, 다시 Refresh Token 탈취에 대해 고민이 필요할 것이다.

그러면 Refresh Token 탈취를 대비한 Refresh Refresh Token을 만들어야 할 것이고, 또 그 토큰의 탈취를 대비한 Refresh x 3 Token... 쭉 이어질 것이다.

결국 Refresh Token은 탈취의 위험을 인정하고, 최대한 Stateless의 이점을 가져가면서 탈취 시 후처리를 추가했다는 개념으로 이해했다.

vs Session

흔히 Refresh Token의 개념을 이야기하면 Session과 다른 점이 뭐냐는 질문이 돌아온다. DB와 같은 저장소에 저장하지 않기 위해 JWT를 썼는데, 다시 저장소에 저장한다니 이상한 결론이다.

여기엔 유효 시간에 재밌는 점이 있다. Session은 인증이 필요한 요청에 대해 저장소를 접근하는데 반해, Refresh Token은 Access Token이 만료될 때까지 저장소에 접근하지 않는다. Access Token의 만료 시간만큼 세션과 저장소 접근 횟수의 차이가 발생한다.

하지만 Refresh Token을 저장하는 시점에서 Refresh Token을 분산 환경에서 어떻게 접근해서 처리할지 고민이 더 필요하다.

RTR (Refresh Token Rotation)

Refresh Token가 탈취되면 후처리로 접근을 막는다지만, 이 방법은 탈취된 사실을 알았을 때 의미가 있다. 그래서 Refresh Token 탈취 자체에 대해서 방지하고 싶을 수 있다. 이런 부분에 대해선 Refresh Token Rotation을 사용할 수 있다.

RTR은 단순하게 말해서, Access Token 재발급 시 Refresh Token을 재발급함으로 Refresh Token을 1회성으로 만드는 방법이다.

1회성으로 만들면서 Refresh Token이 가지고 있는 긴 유효 기간에 대한 문제를 해결한다. 뿐만 아니라 이전에 발급한 Refresh Token을 활용한 Access Token 발급 요청이 다시 들어오면, 해당 Refresh Token의 탈취 여부를 파악할 수 있다.

예를 들어, 사용자가 Refresh Token을 사용하고 해커가 이전 Refresh Token으로 발급 요청을 보내면 공격이 있음을 파악할 수 있다. 반대로 해커가 먼저 Refresh Token을 얻어 재발급 요청을 하면 사용자가 재발급 요청을 보내면서 Refresh Token에 대한 탈취 여부를 파악할 수 있다.

반드시 JWT?

JWT가 유행하다보니 로그인이 들어간 토이 프로젝트에 전부 JWT를 사용하는 모습도 볼 수 있다. 개발자는 충분한 고려와 이유를 가지고 기술을 선택해야 한다.

JWT는 문제가 없을까?

JWT의 구조를 보면 알지만 결국 탈취된 Access Token에 대한 위험을 방지할 수 없다. Refresh Token을 쓰기도 하지만 한 번 크리티컬한 요청을 허용할 수도 있다는 문제가 있다.

그리고 JWT 자체의 길이가 길어 세션에 비해 많은 데이터를 요청에 같이 보내야 하고, 이에 대해 세션에 비해 높은 네트워크 부하가 발생할 수 있다.

Refresh Token에 대한 정보가 저장해서 사용하면서 분산 환경의 세션에서 제시되었던 문제가 동일하게 발생한다. 각 서버는 재발급 요청에 Refresh Token 정보를 가지고 있어야 한다.

이에 인증 서버를 두기도 한다. 물론 세션 서버 방식과 다르게 인증 서버에 접근하는 것은 재발급 요청에만 접근하고 처리되므로 완전히 같은 방식은 아니다. 하지만 해당 서버가 멈추면 인증 정보가 사라지는 등의 문제는 세션 서버와 유사한 문제로 남는다.

결론

보안과 성능은 어느 정도 줄다리기의 이미지를 가진다고 생각한다. 물론 세션에 꽁꽁 싸매어 저장하는 것이 제일 좋은 방법이지만, 제일 중요한 것은 서비스의 성격이다.

빠른 처리를 요구하는 서비스인지, 높은 보안을 요구하는 서비스인지 개발에 앞서 고려하고 채용하는 것이 중요하다고 생각한다.

물론 토이 프로젝트에서 공부를 위해 JWT를 사용할 수 있다고 생각하지만, 기술의 선택엔 이유가 있어야 한다고 느낀다.

profile
데이터를 소중히 여기는 개발자가 되고 싶습니다

4개의 댓글

comment-user-thumbnail
2024년 3월 8일

나 jwt인데 개추 눌렀다

답글 달기
comment-user-thumbnail
2024년 3월 13일

흔히 Refresh Token의 개념을 이야기하면 Session과 다른 점이 뭐냐 > 저한테 들었던 생각인데 잘 읽고 갑니당 ㅎㅎ

답글 달기
comment-user-thumbnail
2024년 3월 16일

오오 JWT에 대해 자세한 설명이네요. 감사합니다.

답글 달기
comment-user-thumbnail
2024년 3월 20일

JWT에 대해 잘 설명해 주셔서 감사합니다. 정말 도움이 되었습니다.

답글 달기