백엔드 개발자(node.js)가 되는 과정

JWT와 토큰 생성, 강제 로그아웃에 대하여

soopy 2024. 2. 14. 23:25
728x90

JWT가 등장한 이유

기존에는 유저의 로그인 상태, 권한 등에 대한 정보를 파악하기 위해 로그인 시 '세션' 이라는 데이터 구조를 생성해 서버에 저장하고, 세션ID를 쿠키에 담아 클라이언트에게 전달해 주는 방식을 취해왔다. 그래서 로그인 상태를 파악하려면 세션ID를 서버에 전달해서 필요한 정보를 확인하는 흐름을 가졌다. 해당 방식은 서버에서 세션을 유지해야 함(stateful)을 의미하는데 이는 서버 메모리, 데이터베이스 혹은 메모리 캐시와 같은 저장소에 보관되었다.

이는 동시 접속 유저 수가 증가할수록 세션 정보를 조회하거나 상태 변경을 위한 요청 횟수가 증가함을 의미하고, 이는 곧 CPU, 메모리 자원 소모가 증가함을 의미한다.

또한 유저 수가 증가하면서 서비스의 규모가 증가하면 서버와 비즈니스의 측면에서 안정성을 보장하기 위해 scale out하여 운영하는 MSA 형태를 취하는데 이 경우 각 서버 간 세션을 공유하고 정합성을 유지하는 것에 어려움이 있다고 한다.

이 문제를 해결하기 위해 Json Web Token이 등장하게 되었다. JWT는 stateless한 접근 방식을 가지는데 유저의 상태 정보를 토큰 형태로 발행하여 쿠키나 로컬 스토리지에 클라이언트가 각자 보관하도록 하여 유저 상태를 관리하는데 사용하는 자원 소모를 줄여주기 때문이다.

JWT 토큰의 구성

JWT는 헤더, 페이로드, 시그니처 이렇게 세 가지 정보를 기반으로 해시화하여 사용한다.

Header - 토근 생성을 위한 해싱(Hashing) 알고리즘과 데이터 타입과 같은 메타데이터를 담는다.

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

Payload - 페이로드에 담기는 정보를 claim이라고 부르는데 주로 JWT에서 표준으로 제공하는 Registered claims을 주로 담는다.

{
  "sub": "userId",
  "iat": "unix timestamp",
  "jti": "hgHgs9j39vhZ"
}

어떤 환경에서 jwt를 설치하여 사용하느냐에 따라 Registered claims의 정보가 다르며 해당 글에서는 Node.js를 기준으로 우리가 활용할 수 있는 Registered claims을 확인해보자

사진첨부

1. **`iss` (Issuer):**
    - 토큰을 발급한 엔터티(issuer)의 식별자를 나타냅니다. 일반적으로 이는 토큰을 발행한 서버의 도메인이나 ID입니다.
2. **`sub` (Subject):**
    - 토큰의 주제(subject)를 나타냅니다. 주제는 토큰을 사용하는 엔터티의 고유 식별자일 수 있습니다.
3. **`aud` (Audience):**
    - 토큰의 대상(audience)을 나타냅니다. 토큰이 의도된 수신자를 가리킵니다.
4. **`exp` (Expiration Time):**
    - 토큰의 만료 시간을 나타냅니다. 이 시간 이후에는 토큰이 더 이상 유효하지 않습니다.
5. **`nbf` (Not Before):**
    - 토큰의 유효 시작 시간을 나타냅니다. 이 시간 이전에는 토큰이 유효하지 않습니다.
6. **`iat` (Issued At):**
    - 토큰이 발급된 시간을 나타냅니다.
7. **`jti` (JWT ID):**
    - JWT의 고유 식별자를 나타냅니다. 토큰의 중복 사용을 방지하기 위해 사용될 수 있습니다.
8. **`typ` (Type):**
    - 토큰의 타입을 나타냅니다. 주로 JWT임을 나타냅니다.

Verify Signature - 클라이언트에서 전달받은 토큰이 유효한지, 신뢰할 수 있는지를 검증하는 서명에 해당하는 부분이다. 헤더와 페이로드를 base64Url 방식으로 인코딩한 값을 Secret 키와 조합하여 헤더에 입력된 해시 알고리즘으로 해시값을 생성한다.

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

결론적으로 "헤더.페이로드.시그니처" 형태의 해시값을 생성한다.

JWT의 검증원리

시그니처에서 우리가 지정하는 Secret이란 값은 서버에 string 형태로 보관한다. 이는 토큰을 서명하는데 사용한다. 서명한 토큰을 클라이언트에 전달한 뒤 이후 클라이언트에서 토큰을 서버에 다시 넘겨주면 서버에서는 토큰에 담긴 헤더와 페이로드 정보를 가지고 서버에서 보관 중인 Secret으로 새로운 토큰을 생성하고, 이 토큰이 클라이언트에서 전달 받은 토큰과의 일치 여부를 판단하여 토큰의 신뢰성을 확보한다.

JWT의 Access Token과 Refresh Token

익히 잘 알고 있겠지만 엑세스 토큰으로 유저를 인증, 인가하고, 해당 토큰이 만료될 경우 리프레시 토큰으로 엑세스 토큰을 재발급한다.

하지만 JWT의 단점은 누군가 탈취했을 경우 즉각적으로 만료시킬 방법이 없지 않은가? 라는 의문을 갖게 한다. 물론 엑세스 토큰의 만료 기간을 짧게 하고, 리프레시 토큰의 만료 기간을 길게 하여 탈취에 대한 취약성을 보완한다고 하지만 예전부터 이러한 논리는 쉽게 납득되지 않았다. 왜냐하면 엑세스 토큰과 리프레시 토큰 모두 쿠키에 저장하기 때문이다. 엑세스 토큰을 탈취한다면 리프레시 토큰도 탈취 못할 이유가 없다.

또한 모든 클라이언트를 강제 로그아웃 시키고자 하면 어떻게 해야할까? 토큰을 강제로 만료시키거나 쿠키를 삭제해야 하는데, 토큰은 각자의 로컬환경에 저장되어 있기 때문에 어찌할 방도가 없다.

이를 보완하기 위해 JWT의 페이로드에 담긴 jti를 활용한다.

jti를 blacklist에 등록

jti는 토큰의 ID를 의미한다. 토큰 생성 시 jti를 함께 생성하여 페이로드에 보관하는데 해당 정보를 token-blacklist 테이블에 보관하고, 입력받은 토큰이 blacklist에 등록되어 있다면 강제 로그아웃 처리가 되도록 하는 방식으로 문제를 해결한다.

728x90
728x90