Home 디지털 입장권 JWT 인증 알아보기
Post
Cancel

디지털 입장권 JWT 인증 알아보기

NHN 아카데미 프로젝트 과정에서 음식 배달 플랫폼을 개발했었는데, 나는 여기서 회원 도메인과 인증을 맡아서 구현했었다.
회원, 주문, 결제 등이 있는 주요 백엔드 서버, 인증 서버, 배치 서버 등 여러 서비스가 나눠져서 구성된 MSA 형태를 가졌었는데, DB는 나누지 않아 완전한 MSA는 아니였다.
이러한 환경에서 인증을 맡아 구현하며 고민했던 내용들과 알아본 기술들을 정리해보려고 한다.

인증 방식


우선, 인증은 사용자 신원을 확인하는 과정이다. 사이트에 내가 회원이자 임을 알리는 과정이라고 볼 수 있다.
HTTP로 인증하는 방식에는 여러 종류가 있었는데, 그 중 가장 간단한 Basic 인증이 있다.

Basic 인증은 아이디, 비밀번호를 헤더에 담아 Authorization : Basic 아이디:비밀번호와 같은 형식으로 서버에 전송한다. 아이디, 비밀번호를 이제부턴 자격증명이라고 하면, 이 자격증명이 Base64로 인코딩되어 헤더에 담겨 보내지는 것이다.

이 과정에서 서버는 전송받은 자격증명과 DB에 정보와 일치하면 사이트로부터 회원임을 인증 받는다. 여기서 주의해야할 점은 Basic 인증은 자격증명을 그대로 노출시킨다는 점이다. Base64 인코딩은 안전하게 전송하기 위한 수단일 뿐, 보안은 적용되어 있지 않다. 따라서 이러한 상황에서는 HTTPS 통신이 필수적이다.

HTTP 인증 출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/Authentication

HTTP는 무상태성을 가지므로, 현재의 요청은 과거의 요청과는 상관없이 독립적이다.
이는 서비스 이용시 매 요청마다 자격증명을 보내 인증을 받아야한다는 것을 뜻한다.
전송받은 자격증명은 DB에 접근하여 저장된 자격증명과 일치여부를 확인하고 인증 처리를 하게 될 것이다.

그렇다면, 서비스 하나가 n번의 요청을 하게 되면 각 요청마다 인증여부를 확인하여 결국 n번의 인증이 필요하게 되는 것이다. 그런데 만약 n번의 요청을 처리하는 서비스가 또 다시 다른 서비스를 호출하는 구조라면, m번의 인증이 추가적으로 요구되면서 총 n x m번의 인증이 필요하다. 예를 들어, 첫 화면에서 로그인하게 되면, 회원 상태 조회, 최근 위치 조회, 주변 음식점 조회, 장바구니 조회 등을 하게 되는데, 이 때 각 요청마다 인증이 필요하게 되어 로그인 포함 총 5번 이상 처리하게 될 것이다.

우리의 서버에서는 이렇듯 n번이였는데, 도메인별로 제대로 나눠져있는 MSA 구조였다면 서로가 서로를 호출하게 되어 n X m번의 인증이 필요하게 됐을 것이다. 이러한 요청은 결국 DB에 엄청난 부하를 주게 되는 작업이다.

이러한 문제를 해결하기위해 서버가 클라이언트와 연결되는 상태를 가지게 하여 DB 접근을 최소화 시킬 수 있다.

이러한 방식으로는 세션 기반 인증이 있다. 세션은 서버의 메모리를 이용하여 클라이언트의 인증 정보를 저장하게 된다. 인증이 성공적으로 이뤄지면, 세션을 생성하고 클라이언트에게 세션ID가 담긴 쿠키를 내려보면서, 클라이언트의 자격증명을 보관하여 인증 요청시 이와 비교하는 것이다. 이를 통해 세션의 정보를 가지고 추가적인 DB 접근을 없앨 수 있다.

그렇다면 스케일 아웃한 경우, 즉 서버가 여러대가 되면 어떻게 될 것인가?

한 서버에 담긴 세션 정보를 다른 서버에서 알 수가 없다. 그리하여 요청이 기존에 이용했던 서버가 아닌 곳으로 가게 되면 인증을 다시 받아야하는 불편함이 있다. 이러한 여러 서버일 때의 문제를 풀기 위해 요청하는 서버를 고정되게 하거나 하나의 공용 세션 저장소를 두어 해결할 수 있다.

이때 세션은 영원히 지속된다면 계속해서 저장소에 쌓이게 될 것이니, 보통은 세션에 유효기간을 두어 사용자의 요청이 일정 시간동안 없으면 갱신되지 않고 삭제되게함으로써 세션 저장소를 관리한다.

스케일 이슈


세션을 통해서 일반적인 인증을 해결할 수 있었다. 그런데, 갑자기 서버가 일괄적으로 다운되거나 인증 트래픽이 급증하는 경우도 생각해 볼 수 있다. 이러한 경우에는 모든 사용자들이 다시 한번 인증을 해야하며, 세션 저장소가 꽉차 사용자 인증에 한계가 다다를 수 있다. 이 때 서버가 아닌 클라이언트에만 상태를 갖게 함으로써 해결해볼 수 있다. 그것에는 토큰 기반 인증이 있다.

이러한 토큰에는 그냥 토큰이 쓰이는 것이 아니고, 전자 서명된 토큰만이 가능하게 된다. 이러한 인증에 쓰이는 토큰으로는 SAML, SWT, JWT 등이 있다.

SAML(Security Assertion Markup Language)
인증 요청과 응답, 메타데이터 등으로 구성되어있는 XML 토큰이다.
SWT(Simple Web Token)
키와 값 쌍으로 이뤄져있고 대칭키 알고리즘으로만 서명되는 가장 단순한 토큰이다.
JWT(Json Web Token)
JSON 구조를 가지며 대칭키 또는 비대칭키 알고리즘으로도 서명될 수 있는 토큰이다.

전자 서명을 가지는 토큰은 서버가 가진 키를 통해 토큰을 검증할 수 있게 한다.

서버는 토큰들을 전부 가지고 있을 필요없이 토큰에 대한 키만 가지고 있으면 된다. 또한 토큰이 변조되거나 위조된 토큰이면 서버가 가진 키를 이용하여 쉽게 검증이 가능하다.

JWT는 다른 토큰에 비해 간략하며 확장성이 높다.

SAML은 XML로 작성되며 주로 여러개의 애플리케이션이 하나의 인증 방식으로 통합하는 SSO(Single Sign On)에 쓰인다. SWT는 정해진 표준이 없어 잘 사용되지 않는다. 그에 비해 JWT는 SAML에 비해 간략하여 통신에서 유리하고, 여러 암호화 방식, 여러 언어 환경에서도 지원하여 충분한 확장성을 가지고 있다. 따라서 도메인 추가시에 늘어나는 서버에도 인증을 적용하기 위해서 JWT를 사용하는 것이 맞아보인다.

JWT


JWT는 {헤더}.{페이로드}.{시그니처}로 이뤄져있다.
헤더에는 토큰의 타입과 알고리즘에 관한 메타데이터가 담기며,
페이로드에는 실제 전송하는 데이터를,
시그니처에는 모두 Base64로 인코딩된 헤더와 페이로드를 시크릿키와 함께 해쉬 함수를 적용시킨 값이 들어간다.
이 때 해쉬 함수는 헤더에 지정된 알고리즘으로 정해진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 헤더
{
  "alg": "HS256",
  "typ": "JWT"
}
// ------------------
// 페이로드
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
// ------------------

위와 같은 JWT는 Base64로 인코딩되어 아래와 같이 변하게 된다. 생성된 JWT 생성된 JWT | 출처 : https://jwt.io/introduction/

만약 변조∙위조된 토큰이나, 다른 서버에서 발급된 토큰이 들어온다면,
토큰의 헤더와 페이로드를 시크릿키와 함께 해싱한 뒤, 그 값이 현재 들어온 토큰의 시그니처값과 동일한 지를 판별하면 된다.
만약 제대로된 토큰이라면 이 값이 일치할 것이고, 그렇지 않다면 다를 것이므로 쉽게 검증이 가능하다.

이로써 토큰을 생성할 때의 키만 알면 서버가 직접 토큰을 가지고 있을 필요없이 인증 처리가 가능해지므로, 스케일 이슈를 해결할 수 있다. 또한 각 서버가 시크릿키만 알면 검증이 가능하므로 세션이 가졌던 저장소 동기화 문제도 피해갈 수 있다.

보안 이슈


토큰을 사용자가 잘 보관한다면 앞선 문제들이 해결되고 별다른 문제가 없어보인다. 하지만 토큰이 자격증명을 대체하는 수단이 되었기에, 이 토큰을 사용자가 탈취당했을 경우 문제가 발생한다.

만약 토큰을 탈취당했다면, 서버는 토큰 검증만 하기에 토큰의 주인과 탈취자를 구분하지 못하게 된다. 그러면 탈취자가 계속 실제 토큰의 주인인 것처럼 서비스를 이용할 수 있다. 이러한 경우를 막을 수 없어보인다. 이미 토큰이 자격증명을 대체했기 때문이다.

그렇지만, 탈취한 토큰을 사용하는데에 제한할 수는 있다. 바로, 토큰에 짧은 제한시간을 두는 것이다. 토큰에 제한시간을 둠으로써 탈취한 토큰은 시간이 지나면 무효화된다. 하지만 토큰의 제한시간은 사용자 본인에게도 동일하게 적용되기에, 곧 잦은 토큰 발급을 하게 만든다. 잦은 토큰 발급은 자격증명을 자주 전송하게 된다는 것을 뜻한다.

자격증명을 자주 전송하는 것은 역시나 위험하다. 손을 자주 넣었다 뺐다하는 주머니에 있는 물건은 쉽게 사라지지 않던가

토큰을 발급받을 때 토큰 재발급만을 위한, 제한시간이 긴 토큰을 같이 발급해주자. 이 토큰이 있으면 자격증명을 전송할 필요없이 제한시간이 갱신된 토큰을 발급받게 된다. 두 토큰을 구분하기 위해 실제 사용자 정보가 담기는 토큰을 액세스 토큰, 액세스 토큰을 재발급 하기 위한 토큰을 리프레쉬 토큰이라한다.

사용자 제한


두 토큰을 사용하게 되면서 서비스 이용은 원활히 진행될 것만 같다. 하지만 여기서 악성 사용자의 접근은 어떻게 막을 수 있을까. 예를 들어, 신고로 인해 7일간 특정 사용자의 접속을 막아야 하는 경우가 생겼다. 토큰이 발급되기 전에 접속하려한다면 충분히 막을 수 있을 것인데, 토큰이 발급된 후라면 이미 클라이언트가 토큰을 가지고 있기에 서버에서는 토큰을 변경할 수 없어 막을 수가 없다.

그렇기에 서버가 이러한 토큰을 알고 있어야만 해당 토큰을 차단할 수 있게 되므로, 어쩔 수 없이 서버가 상태를 가져야만 한다. 토큰의 만료시간과 그 수를 고려하면 차단된 토큰이 스케일 이슈를 일으키진 않을 것이기에, 이러한 경우에는 토큰의 만료기간까지 서버가 상태를 가지는 것이 괜찮아보인다. 하지만 우리의 서버는 여러 대이며 언제든지 늘어날 수 있다. 그렇다면 여러 서버가 같은 상태를 가질 수 있게 할 방법이 필요하다.

또한 사용자가 로그아웃 했을 때에 만료시간이 남은 토큰에 대한 고려도 필요하다. 로그아웃한 뒤 만료시간이 남은 토큰이 재사용될 수 있는 위험을 피하기 위해서이다.

우선, 토큰이 저장되는 특징을 살펴보면 만료기간이 있어 영구저장이 필요없으며, 매우 빈번히 조회하게 될 것이다. 이러한 토큰들을 저장하는 데이터셋을 블랙 리스트라고 한다.

서비스를 호출할 때마다 토큰이 사용되고 인증이 병목지점이 되면 안되니 빠르게 조회해야된다. 이러한 요구사항을 만족시키기위해서 In-Memory Database 기술이 있다. 인메모리 데이터베이스는 서버의 메모리에서 조회하는 것과 비슷한 성능을 내주니 충분히 사용해볼만 하다. 인메모리 데이터베이스에는 대표적으로 Redis, Memcached가 있는데, 저장되는 데이터의 자료구조에 따라 원하는 것을 선택하면 되며, 토큰만을 저장하겠다면, 단순하고 메모리 최적화된 Memcached도 좋은 선택일 수 있다.

마무리


이 밖에도 인증을 어느 서버(게이트웨이, 인증 서버 etc.)에서 처리할 지, 토큰안에 어떠한 정보를 담을 지에 대한 고민도 했었으며, 이 토큰을 어디에 저장할 지에 대한 고민도 했어야 했다. 인증 관련 이슈가 발생하면, 여러 서버에서 디버깅을 해야하는 문제도 겪었었다. 인증을 깊이있게 학습할 수 있었으나, 종종 접수되는 동료들의 401, 403 에러는 나를 불안에 떨게 했다.

처음에 인증을 생각했을 때는 굉장히 간단하고 쉬운 듯 했다. 하지만 구현을 하면서 기술들을 알아나가는데 보안에 대한 내용을 고려하게 되면서 마냥 쉽지 않다는 것을 알게 되었다. 한편으로는, 깊은 지식이 요구되나 결국 외적으로는 당연히 원활히 진행되어야 할 기능이고, 서비스 이용에 있어 중요도가 떨어져보이는 것은 부정할 수 없었다. 그럼에도 불구하고, 평소라면 관심없었을 법한 보안 분야도 흥미롭게 공부할 수 있었고, 서비스 전반을 이해하며 구현해야했기에 좋은 경험이었다.

참고자료

This post is licensed under CC BY 4.0 by the author.

[Java] class의 Inheritance는 상속이 아니다

프로그램 실행:데이터와 인스트럭션으로 하는 핑퐁