임대일

JWT(JSON Web Token): JWT 로그아웃 구현하기(Feat. Stateless) 본문

스프링/시큐리티

JWT(JSON Web Token): JWT 로그아웃 구현하기(Feat. Stateless)

limdae94 2024. 7. 13. 20:04

0. 시작하기 전에

 

JWT(JSON Web Token): (이론) JWT 그리고 스프링 부트 적용하기

0. 시작하기 전에강의 혹은 책에서 흔하게 JWT 토큰과 함께 OAuth2.0 을 함께 사용하여 네이버, 카카오, 구글, 애플 등 서드파티 애플리케이션에게 인증을 위임한다. 아쉽지만 현재 프로젝트에서는 O

limdae94.tistory.com

지난 JWT 이론을 공부하면서 정리한 글이다. JWT 에 대해 자세한 내용을 정리했기 때문에 구체적인 설명은 해당 링크에서 공부할 수 있다.

 

JWT(JSON Web Token)는 클라이언트와 서버 간의 정보를 안전하게 전송하기 위해 사용되는 JSON 기반의 토큰으로 문자열이다. JWT의 주요 특징은 다음과 같다.

 

1. 무상태성(Stateless)
JWT 안에는 정보가 들어있다. 따라서 서버가 세션 상태를 유지할 필요가 없다. 토큰 자체에 사용자와 권한 정보가 포함되어 있어 서버가 별도의 상태를 유지하지 않아도 된다.

 

2. 자체 포함(Self-contained)
JWT는 사용자에 대한 정보를 포함하고 있기 때문에 JWT 만으로 사용자 인증 및 권한 부여가 가능하다. JWT 형식은 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 구성된다.

 

3. 보안성
JWT는 서명을 통해 변조(Tampering)를 방지한다. HMAC, RSA, ECDSA 등의 알고리즘을 사용하여 서명한다. 클라이언트는 토큰을 수정할 수 없으며, 서버는 토큰의 서명을 검증하여 토큰의 무결성을 확인한다.

 

4. 간편한 전달(Compact)
JWT는 URL, HTTP 헤더, POST 요청의 Body 등 다양한 방식으로 쉽게 전달될 수 있는 문자열이다.

 

5. 표준화
JWT는 RFC 7519에 정의된 표준으로 다양한 언어와 프레임워크에서 지원된다.

 

1. JWT 로그아웃 배경

강의 혹은 책에서 흔히 볼 수 있는 로그아웃 시나리오는 단순히 클라이언트가 갖고 있는 JWT 토큰을 삭제하는 것이 전부이다. 좀 더 JWT를 자세히 다룰 경우에는 액세스 토큰(Access Token)과 리프레시 토큰(Refresh Token)으로 구분하고, 토큰 저장소(MySQL 혹은 Redis) 안에 보관된 리프레시 토큰 삭제와 클라이언트가 갖고 있는 JWT 토큰을 삭제하는 정책이다. 그러나 두 방식 모두 치명적인 문제를 갖고 있다.

  1. 공격자로부터 JWT 토큰을 탈취당하면 JWT 토큰은 만료되기 전까지 유효하다.
  2. 탈취당한 JWT 토큰을 막기 위해 새로운 시크릿 키를 등록한다면, 이전 시크릿 키로 생성된 모든 JWT 토큰은 인증이 불가능하다.

첫 번째는 만료 시간 전까지 공격자에 의해 악의적으로 사용될 수 있고, 두 번째는 모든 사용자 재로그인이 필요하다. 해당하는 두 가지의 문제점을 극복하기 위해서는 완전한 Stateless를 만족할 수 없다. 이러한 JWT 인증 방식의 문제점은 개인 프로젝트 진행 중에 부딪히게 되었다.

 

진행 중인 개인 프로젝트에서 로그아웃 기능을 구현하다가 "JWT 토큰 인증 방식에서 가장 적절한 로그아웃 기능 구현은 어떻게 해야 될까?" 고민했다. 이 고민의 배경은 JWT 인증 방식의 가장 큰 특징인 무상태(Stateless)를 만족한 개발을 하기 위해서이다. JWT 토큰을 사용하면서 데이터베이스에 지속적인 접근이 필요하다면, 인증 매커니즘이 세션(Session) 인증 방식과 크게 다를 바가 없다고 느껴졌기 때문이다. 따라서 이 고민을 해결하기 위해 공부를 정리한 글이다.

 

2. JWT 토큰을 무효화

로그아웃 엔드포인트를 구현할 때, JWT 토큰 기반 인증 방식을 사용하는 경우, 단순히 클라이언트 측에서 토큰을 삭제하는 것만으로는 충분하지 않다. 로그아웃 기능을 구현할 때는 보안성을 높이기 위해 서버 측에서도 해당 토큰을 무효화하는 조치가 필요하다. 이렇게 해야 사용자가 로그아웃을 한 후에도 해당 토큰을 재사용할 수 없도록 보장할 수 있다. 일반적으로 JWT 토큰을 무효화하는 일반적인 방법은 다음과 같이 세 가지 방법이 있다.

 

1. 블랙리스트 사용

  • 로그아웃 시 서버 측에 토큰을 블랙리스트에 추가한다.
  • 이후 요청이 들어올 때마다 블랙리스트에 포함된 토큰인지를 확인하는 방법이다.

 

2. 토큰 저장소 사용

  • 토큰을 데이터베이스에 저장하고, 로그아웃 시 해당 토큰의 상태를 무효화로 변경하는 방법이다.
  • 이후 모든 요청에서 토큰의 유효성을 검사할 때 이 저장소를 참조한다.

 

3. 토큰 만료 시간 단축

  • JWT 토큰의 만료 시간을 짧게 설정하고, 리프레시 토큰을 사용하여 새로운 토큰을 주기적으로 발급하는 방법이다.
  • 이렇게 하면 로그아웃 후 짧은 시간 내에 토큰이 자동으로 만료되도록 할 수 있다.

 

소개된 세 가지 방법 중 한 가지만 사용하지 않고, 성능 및 사용자 경험을 고려한 혼합 접근 방식을 사용하는 것이 바람직하다. 또한 Redis와 같은 인메모리 데이터베이스를 사용하여 성능 저하를 최소화하고, 사용자 경험에 영향을 미치지 않도록 설계할 수 있다. 그러나 지금은 Redis까지 사용하지 않고, 혼합 접근 방식을 어떻게 구현하고 공부해야 하는지 정리한다.

 

3. JWT 혼합 접근 방식

JWT의 Stateless 특성을 포기하고 일부분 세션 기반 방식과 유사한 stateful 방식으로 전환하게 된다. 이로 인해 발생할 수 있는 성능 저하와 복잡성을 고려하여 다음과 같은 혼합 접근 방식을 적용할 수 있다. 

 

1. 짧은 수명의 Access Token과 Refresh Token 사용

 

  • 장점: 탈취된 토큰이 유효한 시간이 짧아 보안성이 높아진다.
  • 단점: 토큰 저장소에 리프레시 토큰을 보관하고 관리해야 한다.
  • Access Token: 짧은 만료 시간을 가지며, 주로 API 요청 시 사용한다.
  • Refresh Token: 긴 만료 시간을 가지며, Access Token이 만료되었을 때 새로운 Access Token을 발급받는 데 사용된다.

2. 로그아웃 시 Refresh Token을 블랙리스트에 저장

  • 장점: 로그아웃한 사용자의 Refresh Token을 무효화하여, 해당 토큰으로 새로운 Access Token을 발급받는 것을 방지한다.
  • 단점: 토큰 저장소에 상태 정보를 관리해야 하므로, Stateless의 장점을 일부 포기하게 된다.

3. Access Token이 만료되어 Refresh Token으로 새로운 Access Token을 요청할 때 블랙리스트를 확인

 

  • 장점: 블랙리스트에 있는 Refresh Token을 거부하여 보안 강화한다.
  • 단점: 데이터베이스 조회로 인한 성능 저하 가능성이 있다.

4. 로그아웃 시 Access Token을 블랙리스트에 저장

  • 장점: 탈취된 Access Token을 바로 무효화하여 보안을 강화한다.
  • 단점: 매 요청마다 데이터베이스를 조회해야 하므로, JWT의 Stateless 특성을 상실하고 성능이 저하된다.

 

장점 및 단점 분석

  • 보안 측면: 보안을 강화하는 데 중점을 두고 있으며, 탈취된 토큰의 유효 시간을 최소화하고, 로그아웃한 사용자의 토큰을 무효화하여 보안을 강화하는 효과가 있다.
  • 사용자 경험: Access Token의 짧은 만료 시간은 보안을 강화하지만, 사용자가 자주 로그인하거나 토큰을 갱신해야 하는 불편함이 있다. Refresh Token을 통해 Access Token을 재발급받는 과정에서 블랙리스트를 확인하는 것은 사용자가 모르게 백그라운드에서 처리될 수 있어, 사용자 경험에 큰 영향을 미치지 않는다.
  • 시스템 성능: 블랙리스트를 매 요청마다 확인해야 하는 방식은 데이터베이스 조회로 인해 성능 저하를 초래할 수 있다. 이로 인해, Stateless의 장점을 살리지 못하게 되고, 세션 기반 인증 방식과 크게 다르지 않게 된다.

 

4. 세션과 비교

JWT의 보안을 고려하면 결국 Stateless 를 만족하지 못한다. 그 이유에 대해 정리하자면, Refresh Token 을 토큰 저장소에 보관하여 관리하며 로그아웃 시 Refresh Token을 블랙리스트에 저장하여 바로 무효화시킨다. 그리고 모든 리소스에 접근할 때 마다 유효한 Access Token인지 블랙리스트에 접근해야 한다.

 

그러나 블랙리스트에 Access Token까지 저장하는 경우가 아닌 Refresh Token만 저장하는 경우에는 아직 이점이 있다. 따라서 지금부터 설명하는 JWT 혼합 인증 방식은 로그아웃 시에 블랙리스트에 Refresh Token을 저장하는 방식이라고 생각하면 된다. 세 가지 상황으로 나누어 생각해 보자.

 

1. 최초 로그인에 성공하는 경우

  • 세션 방식: 세션 DB에 접근하여 유저 정보를 확인하고 세션 ID를 반환 받아야 한다.
  • JWT 방식: DB에 접근할 필요 없이 토큰만 발급하면 된다.

 

2. 로그인 상태를 유지하는 경우

  • 세션 방식: 모든 리소스 요청마다 세션 ID가 유효한지 DB를 조회해야 한다.
  • JWT 방식: DB에 별도로 조회할 필요 없이, 서명만 확인해서 토큰이 조작되지 않았는지만 확인하면 된다.

 

3. 로그아웃하는 경우

  • 세션 방식: 세션 DB에서 세션 ID에 해당하는 레코드를 삭제해야 한다.
  • JWT 방식: DB의 블랙리스트 테이블에 로그아웃한 계정의 Refresh Token을 저장하면 된다.

여전히 JWT 방식은 세 가지 중 오직 로그아웃 시에만 데이터베이스에 접근하면 된다. 일부분 Stateful 해지긴 했으나, 여전히 Stateless 의 장점을 잘 살리고 있다고 볼 수 있다. 따라서 Access Token까지 저장하는 경우가 아니라 Refresh Token만 저장하는 경우에는 적절하다.

 

5. 결론

소개한 JWT 혼합 접근 방식을 모두 적용하면 보안 강화 측면에서 매우 효과적이다. 그러나 JWT의 Stateless 특성을 포기하고 세션 기반 방식과 유사한 stateful 방식으로 전환하게 된다. 이로 인해 발생할 수 있는 성능 저하와 복잡성을 고려하여 다음과 같은 혼합 접근 방식을 채택했다.

  1. 짧은 수명의 Access Token과 Refresh Token 사용
    • Access Token의 만료 시간을 짧게 설정하여 보안을 강화합니다. (예: 30분 / 14일)
    • Refresh Token을 사용하여 새로운 Access Token을 발급받는다.
  2. 로그아웃 시 Refresh Token을 블랙리스트에 저장
    • Refresh Token을 블랙리스트에 저장하여 무효화하고, 새로운 Access Token 발급을 막는다.
  3. 블랙리스트 확인 최적화
    • 매 요청마다 데이터베이스를 조회하는 대신, 블랙리스트 정보를 캐싱하거나 Redis와 같은 인메모리 데이터베이스를 사용하여 성능 저하를 최소화한다.
  4. 시크릿 키 주기적 회전
    • 시크릿 키를 주기적으로 회전하여 보안을 강화하되, 이를 신중하게 관리하여 사용자 경험에 영향을 미치지 않도록 한다.(예: 매 월, 분기)
  5. IP 주소 혹은 기기 정보를 토큰에 포함 (추가 보안 요소) 
    • 추가적인 보안 요소를 채택할 수 있다. IP 주소 혹은 기기 정보를 토큰에 포함시켜 토큰을 사용하는 클라이언트의 신원을 확인할 수 있다. 이를 통해 탈취된 토큰이 다른 기기 혹은 IP 주소에서 사용되는 것을 방지한다.

 

보안을 강화하기 위해 JWT 혼합 접근 방식을 최대한 많이 고려하면 효과적이지만, 성능 및 사용자 경험을 고려한 혼합 접근 방식을 사용하는 것이 좋다. JWT의 Stateless 특성을 최대한 유지하면서도 블랙리스트를 사용하여 보안을 강화할 수 있는 최적화된 방식을 채택하는 것이 좋다. Redis와 같은 인메모리 데이터베이스를 사용하여 성능 저하를 최소화하고, 사용자 경험에 영향을 미치지 않도록 설계할 수 있다.