JSON Web Token의 약자인 JWT는 온라인 네트워크에서 정보를 안전하게 통신할 때 사용하는 인터넷 표준 토큰이다. JWT는 인증, 정보 교환 등 다양한 용도에 사용된다. 이때 주고받는 정보를 클레임(Claim)이라고 하고, 클레임의 집합은 JSON 객체로 표현한다.
1. JWT의 두 가지 유형
JWT에는 두 가지 유형이 있다. 더 널리 사용되는 첫 번째 방식은 JWS(JSON Web Signature)이다. JWS 방식을 사용하면 클레임의 내용을 누구나 읽을 수 있지만, 서명되어 있기 때문에 데이터의 무결성이 보장된다. 두 번째 방식인 JWE(JSON Web Encryption)이다. JWE에서는 클레임 자체를 암호화한다. 그래서 복호화 방법을 알고 있어야만 페이로드를 읽을 수 있다.
보안 측면에서는 클레임을 암호화하는 JWE 방식이 더 안전하다. 하지만 페이로드의 데이터를 클라이언트가 바로 사용해야 한다면 JWS가 더 편리하고 데이터의 무결성을 보장할 수 있다. 즉, 보안, 사용성 등을 고려하여 JWT 방식을 선택해야 한다.
2. JWT의 구성
JWS, JWE 둘 다 구성은 같다. 헤더, 페이로드, 서명 세 가지 주요 요소로 구성되고 각 구성은 마침표(.)로 구분된다.
- 헤더(Header): 일반적으로 헤더에는 토큰의 유형(JWS, JWE)과 서명 알고리즘을 명시한다. JSON으로 표현된 헤더를 Base64로 인코딩한 것이 JWT 헤더이다.
- 페이로드(Payload): 보통 JSON 형식으로 표현된 사용자 정보나 클레임이 key-value로 포함된 부분이다. RFC 7519에 정의된 iss, exp, sub, aud 등의 키를 사용할 수도 있고, 필요에 따라 새로운 클레임을 추가할 수도 있다. JWS 방식에서는 페이로드도 Base64로 인코딩한다. 참고로, 누구나 디코딩할 수 있기 때문에 JWS 페이로드에는 민감한 정보를 넣으면 안된다. JWE 방식에서는 페이로드를 알고리즘과 비밀키로 암호화하기 때문에 민감한 정보를 포함할 수 있다.
- 서명(Signature): 헤더와 페이로드를 결합한 후 지정된 알고리즘과 비밀키 또는 공개키로 서명한 값이다. 이 서명을 통해 JWT의 무결성이 보장되며, JWT가 변경되지 않았음을 확인할 수 있다. 서명은 아래와 같은 형태이다. 인코딩한 헤더와 페이로드를 헤더에 정의한 알고리즘을 통해 secret(키)로 암호화한다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
3. JWT의 보안 문제
3-1. 토큰 탈취 시 Stateless하기 때문에 발생하는 문제
토큰 방식의 인증에서 주로 JWT가 토큰으로 사용된다. 그러나 사실 JWT는 인증을 위한 것이 아니라 신뢰할 수 있는 데이터 전송을 위한 것이다. 즉, 신뢰할 수 있는 데이터 전송을 위한 것이지 인증을 위해 만들어진 것은 아니다. 그렇기 때문에 JWT 인증 방식에는 여러 단점과 한계들이 존재한다.
우선 JWT는 세션 방식과 달리 Stateless하다는 장점을 갖는다. 세션 방식은 매 요청마다 세션 DB를 찔러 세션에 대한 유효성을 확인한다. 그러나 토큰 방식은 DB를 찌를 필요 없이 JWT만으로도 유효성 검증이 가능하다. 따라서 인증 로직으로 인한 서버의 부하를 줄일 수 있다.
그러나 Stateless하기 때문에 한 번 발급한 토큰에 대해 서버는 제어권을 갖고 있지 않다. 따라서 JWT 토큰이 탈취되면 해커는 사용자인척 리소스에 접근할 수 있으며, 서버측에서는 토큰 시간이 만료되길 기다릴 뿐 토큰을 만료시킬 수 없다.
3-2. Refresh Token의 등장
앞서 언급한 JWT 토큰의 보안 문제를 해결하기 위해, Refresh Token(RT)과 Access Token(AT)으로 나누어 토큰을 발급하는 방식이 있다. 앞서 언급했듯이 토큰 방식은 Stateless 하기 때문에 탈취당하면 서버측에서는 토큰 만료시간을 기다리는 것 외에는 별다른 대응을 할 수 없다. 따라서 탈취 위험을 낮추기 위해 토큰 만료시간을 짧게 설정하면 된다. 그러나 만료 시간을 짧게 설정하면 사용자는 빈번하게 재로그인을 해야하므로 번거롭다.
따라서 사용자 검증을 위한 용도인 AT과 AT을 재발급하는 용도인 RT을 구분하고, 이때 AT의 만료시간은 짧게, RT의 만료시간은 길게 설정하면 된다. 즉 AT의 만료시간을 짧게 설정하여 토큰 탈취의 위험을 낮추고, RT을 통한 AT 재발급을 통해 만료시간이 짧은 AT이 만료되었을 때 사용자가 재로그인하는 번거로움을 줄일 수 있다.
3-3. RTR(Refresh Token Rotaion) 방식
AT 탈취 위험성을 낮추기 위해 RT을 활용한다. 그런데 RT이 탈취되면 어떻게 될까? 해커는 탈취한 RT으로 AT을 발급받아 사용자인척 리소스에 접근할 수 있다. RT 탈취 위험을 낮추기 위한 방법으로 RTR방식이 있다. RTR은 RT을 통해 AT을 재발급받을 때 RT도 재발급하는 방법이다. 즉 RT을 한 번만 사용하는 방법이다.
- 예를 들어, 최초 로그인시 서버는 AT과 RT을 사용자에게 발급해준다. 그리고 이때 RT을 사용자 DB에 저장해둔다.
- AT이 만료되어 사용자는 RT을 전달하며 새로운 AT발급을 요청한다.
- 서버는 전달받은 RT 자체의 유효성 검증과 함께 DB에 저장된 사용자의 RT과 전달받은 RT을 비교한다.
- 전달받은 RT이 유효하면서 DB의 RT과도 일치하면 새로운 AC과 RT을 발급한다. 그리고 DB에 저장된 RT 값을 재발급된 RT 값으로 변경한다.
RTR 방식을 적용하면, 해커가 RT을 탈취해서 AT 재발급을 요청하더라도 가장 최신의 RT이 아닌 이전 버전의 RT이면 요청을 거절한다. 즉, RT이 만료되기까지 기다리는 방법 외에도 가장 최근 발급된 RT가 아니면 요청을 거절하므로 좀 더 안전하다.
그러나 여전히 다음 두가지 문제가 있다.
- RT 탈취
- 새롭게 발급받은 RT가 탈취되어, 아직 사용하지 않은 RT가 탈취된다면 해커는 1회 한정으로 AT을 발급받을 수 있다.
- 만료시간 내에 AT이 탈취될 경우
- 아무리 RT을 통해 AT의 만료시간을 짧게 설정한다고 하더라도, 그 짧은 만료시간 내에 AT을 탈취하여 악용할 수 있다.
3-4. JWT 블랙리스트
로그아웃 된 사용자의 토큰 또는 탈취가 의심되는 토큰을 블랙리스트로 DB에 등록하는 방법도 있다. 그리고 요청에 대해 해당 토큰이 블랙리스트에 등록되어 있는지 확인 후, 등록되어 있다면 요청을 수행하지 않는다.
그런데 AT을 블랙리스트로 등록하면 문제가 있다. 바로 매 요청마다 해당 AT가 블랙리스트에 있는지 확인해야 하는데 이는 매번 DB를 찌르는 방식으로 세션 방식과 크게 다를 것이 없다. 즉, 완전히 Stateful하게 되어버린다. 따라서 RT만 블랙리스트로 등록하는 방법이 있다. 즉, AT은 만료시간을 짧게 두어 탈취의 위험을 낮추고, 만료시간이 비교적 긴 RT에 대해서만 블랙리스트로 관리하는 것이다. 이렇게 하면 RT을 통해 AT을 갱신하는 요청에 대해서만 블랙리스트 DB를 찌르면 되므로 완전히 Stateful하지는 않다.
4. 정리
위 내용을 토대로 프로젝트에는 다음과 같이 적용했다.
- AT의 만료시간을 짧게 설정하고 RT을 별도로 둔다.
- → AT 탈취시 위험성을 낮춘다.
- RT을 DB에 저장해두고, RT를 통해 AT 갱신 요청이 오면 기존 RT를 DB에서 제거하고 새로운 RT를 저장한다.(RTR 방식)
- → RT 탈취 시 이미 사용된 RT라면 유효하지 않으므로 요청이 거절된다.
- 로그아웃 등 토큰에 대한 제어가 필요한 상황인 경우, RT를 DB에서 제거한다.
- → 이후 RT를 통한 요청이 들어오면 유효하지 않으므로 요청이 거절된다.
그러나 여전히 다음과 같은 한계를 갖는다.
- 아직 사용하지 않은 RT가 탈취된다면 해커는 1회 한정으로 AT을 발급받을 수 있다.
- AT 만료시간이 짧더라도 아직 만료되지 않은 AT가 탈취될 수 있다.
- 이때 AT도 DB에 저장해두고 블랙리스트로 관리한다면, 블랙리스트에 등록된 AT 요청은 거절할 수 있기 때문에 더 안전하다. 그러나 매 요청마다 블랙리스트 DB를 찔러 확인해야 하므로 서버 부담이 커지고 매 요청마다 세션 DB를 찌르는 세션 방식과 차이가 없어진다.
위 방식은 RT을 DB로 관리하여 좀 더 Stateful하게 설계하여 안정성을 높인 방식이다. 그런데 이렇게 하면 "Stateful하게 설계한다면 세션 방식을 사용하면 되지 않나? 굳이 토큰 방식을 적용해야 하는 이유가 뭐지?" 라는 생각이 들 수 있다. 그러나 다음과 같이 3가지 경우를 비교했을 때 세션 방식에 비해 토큰 방식이 DB에 접근하는 횟수가 적다. 따라서 약간은 Stateful해졌지만 여전히 Stateless의 장점을 갖는다고 볼 수 있다.
- 로그인 성공 시
- 세션: 세션 DB에 접근해 유저 정보를 저장해야 한다.
- JWT: DB에 접근할 필요 없이 토큰만 발행하면 된다.
- 로그인이 되어있는 경우
- 세션: 매요청마다 세션이 유효한지 세션 DB에 접근해야 한다.
- JWT: DB에 접근할 필요 없이 JWT의 유효성만 검증하면 된다.
- 로그아웃 시
- 세션: 세션 DB에서 해당 세션 정보를 삭제해야 한다.
- JWT: RT가 저장된 DB에서 해당 RT을 제거하면 된다.
Reference