여기서는 JWT(Json Web Token)에 대해서 다룰 것입니다.
해당 페이지([Security] 쿠키와 세션)에서 세션 로그인을 하는 방법에 대해서 설명 하였고, 그것이 가지고 있는 한계에 대해서 정리를 했습니다.
이것을 극복하기 위해서 Token 기반의 인증에 대해서 정리를 하겠습니다.
토큰 기반 인증 시스템은 클라이언트가 서버에 접속을 하면 서버에서 해당 클라이언트에게 인증되었다는 의미로 '토큰'을 부여합니다. 이 토큰은 유일하며 발급받은 클라이언트는 또 다시 요청을 보낼 때 '선택적으로' 요청 헤더에 토큰을 담아서 보냅니다.
인증이 필요한 요청의 경우, 해당 토큰을 파싱(parsing)하는 과정을 통해 사용자 인증 작업을 수행합니다.
이전에 봤던 세션기반 인증은 서버가 파일이나 데이터베이스에 세션정보를 가지고 있어야 하고 이를 조회하는 과정이 필요하기 때문에 많은 오버헤드
가 발생합니다. 하지만 토큰은 세션과 달리 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수 있습니다.
토큰 자체에 데이터가 들어있기 때문에 클라이언트에서 받아 위조되었는지 판별만 하면 됩니다.
⚡️ 파일이나 데이터베이스 조회로 인한 오버헤드 ⚡️
세션 기반을 통해서 데이터를 저장하려면 서버에 저장을 해야한다고 정리를 했었습니다. 서버에 저장되기 위해서 특정 경로에 있는 파일에 해당 정보를 담아둘 것이고, 이것은 파일 시스템을 호출하는 "시스템 콜"에 의해서 수행될 것입니다.
만약, 확장성을 위해서 이것을 데이터베이스에 저장을 해서 사용을 한다면 데이터베이스에서 가져오는 작업을 요구하게 될 것입니다.
이러한 두 작업은 I/O 작업을 요구하며 I/O 작업의 경우 서버에서 수행하는 기능 중 거의 가장 느린 작업에 속하므로 이것은 속도적인 측면에서 좋지 않습니다.
토큰 기반 방식은 서버 기반 방식과 다르게 서버에 정보를 저장하지 않으므로 stateless
한 것입니다.
동작의 순서는 다음과 같습니다.
토큰의 단점은 다음과 같습니다.
3번의 경우, 토큰의 기간을 짧게 가져가고, CSRF 토큰도 같이 사용하면서 문제를 해결할 수 있습니다.
JWT(JSON Web Token)은 웹 표준(RFC 7519)으로, 두 당사자 사이에서 정보를 JSON 객체로 안전하게 전송하기 위해 디자인한 컴팩트한 토큰입니다.
JWT는 JSON 데이터를 BASE64 URL-safe Encode를 통해 인코딩하여 직렬화 한 것으로, 토큰 내부에는 위변조 방지를 위한 전자 서명도 들어 있습니다.
Spring Boot 프로젝트에서 Spring Security를 이용하여 JWT를 발급하여 사용하려고 할 때,
암호화 알고리즘
을 적는 매개변수 칸이 존재합니다. 이것은 페이로드를 암호화하려고 하는 것이 아니라는 점을 명확히 해둬야 합니다. 이것은 전자서명을 위해 사용됩니다.
JWT 구조는 아래와 같습니다.
구분자로 .이 사용되고, Header.Payload.Signature 의 구조를 갖고 있습니다.
JWT의 특징을 정리하자면 다음과 같습니다.
서명의 경우, 대칭키 암호화 방식과 공개키 암호화 방식에서 선택할 수 있으며, 대칭키 방식은 공개키 방식에 비해서 동작 수행이 빠르지만 키를 공유하기 때문에 보안적으로 위험할 수 있습니다.
만일, 서명을 만드는 키가 탈취당할 경우 매우 위험해질 수 있습니다.
공개키 방식은 오직 "비밀키"만을 사용하여 암호화를 수행하고 공개키를 공유하면서 복호화 작업을 수행하기 때문에 보안상 더 안전합니다. 하지만, 대칭키 방식보다는 속도적인 측면에서 느립니다.
애플리케이션이 분산된 구조여서 사용자 검증이 여러 서버에서 이루어지는 상황이라면키를 공유해야 되는 상황이 원격으로 이루어 질 수 있습니다.
이러한 경우, 공개키 방식을 사용한다면 대칭키보다 훨씬 안전하게 키를 주고 받으면서도 보안을 챙겨갈 수 있습니다.
시그니처(서명)의 구조는 헤더
와 페이로드
와 서버가 가지고 있는 시크릿 키
를 사용하여 암호화합니다.
유저 JWT: A(Header) + B(Payload) + C(Signature)일 때 누군가 토큰을 탈취하여 B를 수정했다고 해봅시다.
앞서 말했지만, 토큰은 '정보 보호'의 목적을 위해 사용하기 보다 '신뢰성' 즉, 위조 방지를 위해서 사용합니다.
마지막으로 JWT의 장단점을 정리해보면 다음과 같습니다.
장점
2번, 3번과 4번으로 얻을 수 있는 가장 큰 이점은 인증 절차에서 DB 조회를 할 필요가 없다는 것입니다.
서버 자체가 죽는 경우도 있지만, 대부분 DB가 터져서 서버도 같이 죽는 경우가 많습니다.
이런 점에서, JWT에 사용자 고유 인증 정보(PK)와 권한을 담아서 보낸다면 이를 통해 많은 오버헤드를 줄일 수 있습니다.
단점
4번의 단점으로 인해서 토큰 자체가 탈취당한 경우, 다른 공격자가 해당 토큰을 가지고 다른 사람 흉내를 내서 각종 작업을 수행할 수 있습니다.
이것은 매우 위험한 상황을 유발할 수 있으므로 무조건 피해야 합니다. 이 문제를 해결하기 위해서 토큰이 탈취당하더라고 사용하지 못하도록 막을 수 있어야 합니다.
따라서 토큰의 만료 기간을 짧게 가져가서 토큰을 탈취하더라도 만료 기간이 다되도록 하여 해당 문제를 해결할 수 있습니다.
하지만, 또 다른 문제가 생기는데 만료 기간을 짧게 가져갈 경우에 사용자가 만료 기간보다 더 오랫동안 해당 토큰을 애플리케이션에서 사용하려고 하는 경우 토큰이 도중에 만료가 될 것입니다.
토큰이 만료되었기 때문에 사용자는 토큰을 재발급받아야 하고, 위에서 봤던 로직대로라면 로그인 절차를 다시 수행해야 합니다. 이것은 사용자에게 불편함을 제공하므로 보안만큼이나 큰 문제입니다.
여기서 Refresh Token
의 개념이 등장합니다.
Refresh Token은 Access Token(JWT)를 재발급 받기 위해서 사용되는 토큰입니다.
Refresh Token은 Access Token과 같은 JWT입니다. 단지 Access Token은 접근에 관여하는 토큰이고, Refresh Token은 재발급에 관여하는 토큰이므로 역할이 다를 뿐입니다.
Refresh Token은 목적에 맞게 Access Token보다 상대적으로 긴 유효시간을 가져야 하고, 이것은 애플리케이션 정책에 맞게 설정될 것입니다.
Access Token이 만료되었을 경우 해당 토큰을 재발급 받기 위해서 서버에서는 Refresh Token을 확인하는 절차를 수행해야하고, 해당 작업을 수행하기 위해서는 DB에 저장되어 있는 사용자의 고유 정보(PK)와 매칭된 Refresh Token의 값과 비교해야 합니다.
먼저, 로그인을 할 때 Access Token
과 Refresh Token
을 발급 받습니다.
이후 사용자가 사용하고 있던 Access Token을 가지고 검증을 수행하다가 만료기간이 지난 경우 재발급 로직을 수행해야 합니다.
재발급 로직은 저장되어 있는 Refresh Token과 사용자가 보내온 Refresh Token을 비교하여 일치한 경우, 새로 Refresh Token으로 업데이트 해주고 Access Token도 새롭게 재발급하여 두 토큰을 사용자에게 보냅니다.
사용자는 별도의 재로그인 과정을 수행할 필요 없이 계속해서 서비스를 이용할 수 있습니다.
Refresh Token은 어떠한 경우에 사용될지 상황을 분석해 봅시다.
(해당 상황은 Access Token과 Refresh Token을 사용하는 경우에 발생하는 CASE로 설계시 고려해야 합니다.)
1번이 경우, 정상적으로 처리해주면 됩니다. 별다른 조치를 취할 필요 없이 서명만 검증하면 됩니다.
2번의 경우, Refresh Token을 검증하여 Access Token을 재발급 받아야 합니다. Spring Security를 수행하는 경우 Access Token의 만료 기간이 지났다면 에러를 터트립니다. 이것을 잡아서 Refresh Token을 재발급 해주는 로직을 수행해 줘야 합니다.
3번의 경우, 두가지 방식으로 나눌 수 있습니다.
3.1 Refresh Token을 재발급
3.2 정상 로직 처리
3.1번의 방식대로 처리를 하는 경우 모든 로직이 변경되어야 한다는 것을 알아두어야 합니다. 왜냐하면 가장 기본 절차는 Access Token의 검증만 수행하면 되는 것이였는데, 검증이 완료되었음에도 불구하고 "재발급" 목적으로 사용하는 Refresh Token까지 검증을 수행해야 3번의 상황이 발생할 수 있습니다.
하지만, 이 방식대로 할 경우 사용자는 이후 Refresh Token이 만료되도 Access Token을 재발급 받았기 때문에 2번의 상황대로 다시 Refresh Token을 발급받을 수 있으므로 사용자에게 편의성을 제공해줍니다. (재로그인해야 되는 확률이 줄어듬)
3.2번의 방식은 Access Token만 검증을 수행하는 로직으로 구현하면 되고, Access Token의 검증이 통과되었다면 굳이 Refresh Token의 검증을 수행하지 않습니다.
4번은 두 토큰 다 유효하지 않으므로 사용자가 로그인을 통해 새롭게 재발급 받아야 합니다.
인증 과정은 아래의 사진을 통해 확인할 수 있습니다.
별다른 설명을 적을 필요가 없어 보이므로 넘어가겠습니다.
해당 기법은 다음과 같은 문제 상황을 해결하기 위해 사용하는 방법입니다.
만약, 공격자가 만료된 AccessToken을 훔친 뒤 아직 만료 기간이 많이 남은 Refresh Token을 훔쳤다고 가정해 봅시다.
공격자는 이 두 토큰을 서버에 보내면 새로운 Access Token을 발급해 주기 때문에 매우 위험한 상황이 발생합니다.
위의 사진에서도 보이듯이 Access Token의 재발급 절차를 거치는 과정에서 사용자에게 주는 것은 오직 Access Token만 재발급해서 줍니다. 즉, Refresh Token은 만료 기간이 지났지 않았으면 굳이 재발급해주지 않습니다.
하지만, 이것은 장기간동안 동일한 Refresh Token을 사용할 수 있으며 사용자는 아무런 만료된 Access Token을 가지고 유효한 Refresh Token을 탈취하기만 한다면 언제든지 새롭게 Access Token을 재발급 받을 수 있다는 것입니다.
이러한 문제를 피하기 위해서 Access Token을 발급해주는 과정이 생길때 마다 Refresh Token을 새롭게 업데이트 해주는 작업을 추가할 수 있습니다.
이 두 방식은 Trade-Off 관계를 가지고 있는데,
여기서는 본격적으로 보안적인 측면을 고려하여 Access Token과 Refresh Token을 어디에 저장해야 될지 고려를 해봅시다.
(여기서부터 필기는 필자의 고민을 바탕으로 작성된 내용이 많습니다.)
먼저, 두 토큰의 저장 가능한 위치를 적어봅시다.
여기서는 통신 과정에서 SSL/TLS 암호화 프로토콜을 적용한 것으로 예를 들겠습니다.
사실, 1번과 2번을 사용하는 것은 보안상 매우 취약해질 수 있습니다. HTTPS 방식을 사용하기 때문에 패킷 탈취는 힘들다고 하지만, XSS(Cross Site Script) 방식을 통해서 로컬 스토리지와 쿠키에 접근할 수 있기 때문에, 보안상 위험합니다.
하지만, 3번을 사용할 경우 스크립트를 사용한 접근은 불가능합니다. 하지만, 쿠키는 자동 전송 기능이 있기 때문에 이것으로 인해서 CSRF(Cross Site Request Forgery)를 통해 악용될 가능성이 존재합니다.
이 문제까지 해결하기 위해서 CSRF Token까지 고려를 한다면 문제가 다소 해결될 수 있지만 현재 페이지에서는 다루지 않을 것입니다.
최종적으로 Access Token과 Refresh Token을 사용한 경우, 가장 안전한 경우는 3번의 방식인 HTTP Only 설정을 걸어둔 쿠키에 저장하는 것이 가장 안전합니다.