들어가며
항상 프로젝트를 진행하면서, 회원가입 기능을 만들 때, JWT를 무지성으로 활용했습니다. 왜 JWT를 사용해야 하는지, JWT를 사용하면 어떤 문제가 있는지, 왜 세션은 고려하지 않았던 것인지 단 한 번도 제대로 생각해본 적 없다는 생각이 들었습니다. 기본적인 부분도 모르는 개발자라는 생각이 들었습니다. 이번 기회에 JWT는 무엇이고, 왜 JWT를 사용하는지, JWT는 안전한지, 더 나은 회원가입 방법은 무엇인지 정리해보려 합니다.
쿠키 & 세션의 장단점
JWT를 살펴보기 전, 왜 JWT를 사용하는 것인지 알아보겠습니다. JWT가 아닌, 쿠키, 세션을 활용한 인증 인가 방식을 구현한다면 구현이 상당히 명확하며, 서버에서 유저 상태 확인이 편리합니다. 또한 상대적으로 안전하며, 서버 측에서 관리하기 때문에 클라이언트 변조에 영향을 받거나 데이터의 손상의 우려가 없습니다. 또한 특정 세션을 탈취한 유저가 들어왔을 때 세션 만료를 시켜서 강제 로그아웃을 시킬 수 있습니다.
하지만 서버에서 세션 저장소를 활용해야 하기에, 서버에서 추가적인 저장공간을 필요로 하게 되고, 이는 자연스럽게 부하를 증가시키는 요인이 됩니다. 또한 사용자가 늘어나면 더 많은 트래픽을 처리하기 위해 여러 프로세스를 돌리거나 컴퓨터를 추가하는 등 서버를 확장해야 합니다. 세션을 사용한다면 세션을 분산시키는 시스템을 설계해야 하지만 이런 과정은 어렵고 복잡합니다. 또한 해커가 HTTP 요청을 가로채서, 쿠키를 훔치고 훔친 쿠키를 이용해서 HTTP 요청을 보내면 서버의 세션 저장소에서는 특정 사용자로 인식하고 정보를 전달합니다. 즉 세션 하이재킹 공격이 발생할 수 있습니다. (물론 이 부분은 HTTPS를 사용하고, 세션에 유효시간을 넣어준다면 조금은 해결할 수 있는 문제입니다.) 그럼 JWT는 이 부분을 어떻게 해결하고 있을까요? 이에 대해 알아보겠습니다.
JWT란?
JWT는 Json Web Token의 약자로 인증에 필요한 정보들을 암호화시킨 토큰을 뜻합니다. 위의 세션/쿠키 방식과 유사하게 사용자는 Access Token(JWT 토큰)을 HTTP 헤더에 실어 서버로 보내게 됩니다.
Header : 위 3가지 정보를 암호화할 방식(alg), 타입(type) 등이 들어갑니다.
Payload : 서버에서 보낼 데이터가 들어갑니다. 일반적으로 유저의 고유 ID값, 유효기간이 들어갑니다.
Verify Signature : Base64 방식으로 인코딩한 Header, payload 그리고 SECRET KEY를 더한 후 서명됩니다.
최종적인 결과 : Encoded Header + "." + Encoded Payload + "." + Verify Signature
Header, Payload는 인코딩 될 뿐(16진수로 변경), 따로 암호화되지 않습니다. 따라서 JWT 토큰에서 Header, Payload는 누구나 디코딩하여 확인할 수 있습니다. 여기서 누구나 디코딩할 수 있다는 말은 Payload에는 유저의 중요한 정보(비밀번호)가 들어가면 쉽게 노출될 수 있다는 말이 됩니다.
하지만 Verify Signature는 SECRET KEY를 알지 못하면 복호화할 수 없습니다. A 사용자가 토큰을 조작하여 B 사용자의 데이터를 훔쳐보고 싶다고 가정하겠습니다. 그래서 payload에 있던 A의 ID를 B의 ID로 바꿔서 다시 인코딩한 후 토큰을 서버로 보냈습니다. 그러면 서버는 처음에 암호화된 Verify Signature를 검사하게 됩니다. 여기서 Payload는 B 사용자의 정보가 들어가 있으나 Verify Signature는 A의 payload를 기반으로 암호화되었기 때문에 유효하지 않는 토큰으로 간주하게 됩니다. 여기서 A 사용자는 SECRET KEY를 알지 못하는 이상 토큰을 조작할 수 없다는 걸 확인할 수 있습니다. 그럼 이제부터 JWT가 어떻게 인증에 사용되는지 알아보도록 하겠습니다.
JWT의 인증 과정
JWT의 인증 과정에 대해 살펴보겠습니다.
1. 사용자가 로그인을 한다.
2. 서버에서는 계정 정보를 읽어 사용자를 확인 후, 사용자의 고유한 ID값을 부여한 후, 기타 정보와 함께 Payload에 넣습니다.
3. JWT 토큰의 유효기간을 설정합니다.
4. 암호화할 SECRET KEY를 이용해 ACCESS TOKEN을 발급합니다.
5. 사용자는 Access Token을 받아 저장한 후, 인증이 필요한 요청마다 토큰을 헤더에 실어 보냅니다.
6. 서버에서는 해당 토큰의 Verify Signature를 SECRET KEY로 복호화한 후, 조작 여부, 유효기간을 확인합니다.
7. 검증이 완료된다면, Payload를 디코딩하여 사용자의 ID에 맞는 데이터를 가져옵니다.
JWT의 장단점
장점은 첫 번째 간편합니다. 세션/쿠키는 별도의 저장소의 관리가 필요하지만, JWT는 발급한 후 검증만 하면 되기 때문에 추가 저장소가 필요 없습니다. 이는 Stateless 한 서버를 만드는 입장에서는 큰 강점입니다. HTTP의 stateless는 서버와 클라이언트는 클라이언트가 이전에 요청한 결과에 대해 잊어버려야 한다는 것입니다. 만약 클라이언트가 이전 요청과 같은 데이터를 원한다면 다시 서버에 연결해서 동일한 요청을 해야 합니다. 만약 세션을 활용하면 세션 저장소를 왕복해야만 하는데, 이때 JWT를 활용하면 매번 데이터베이스를 왕복해서 인증할 필요가 더욱 줄어듭니다. 이는 서버를 확장하거나 유지, 보수하는데 유리합니다. 하지만 Verify Signature는 SECRET KEY를 알지 못하면 복호화할 수 없습니다. A 사용자가 토큰을 조작하여 B 사용자의 데이터를 훔쳐보고 싶다고 가정하겠습니다. 그래서 payload에 있던 A의 ID를 B의 ID로 바꿔서 다시 인코딩한 후 토큰을 서버로 보냈습니다. 그러면 서버는 처음에 암호화된 Verify Signature를 검사하게 됩니다. 여기서 Payload는 B 사용자의 정보가 들어가 있으나 Verify Signature는 A의 payload를 기반으로 암호화되었기 때문에 유효하지 않는 토큰으로 간주하게 됩니다. 여기서 A 사용자는 SECRET KEY를 알지 못하는 이상 토큰을 조작할 수 없다는 걸 확인할 수 있습니다. 그럼 이제부터 JWT가 어떻게 인증에 사용되는지 알아보도록 하겠습니다.
두 번째 장점은 확장성이 뛰어나다는 것입니다. 토큰 기반으로 하는 다른 인증 시스템에 접근이 가능합니다. 예를 들어 Facebook 로그인, Google 로그인 등은 모두 토큰을 기반으로 인증합니다. 이에 선택적으로 이름이나 이메일 등을 받을 수 있는 권한도 받을 수 있습니다.
물론 여기까지 봤을 때는 JWT가 세션/쿠키 방식보다 더 효율적으로 보이지만, JWT에도 단점들이 존재합니다. 첫 번째 단점은 이미 발급된 JWT에 대해서는 돌이킬 수 없다는 것입니다. 세션/쿠키의 경우 만일 쿠키가 악의적으로 이용된다면, 해당하는 세션을 지워버리면 됩니다. 하지만 JWT는 한 번 발급되면 유효기간이 완료될 때까지는 계속 사용이 가능합니다. 따라서 악의적인 사용자는 유효기간이 지나기 전까지 정보를 가져갈 수 있습니다. 이 문제를 해결하기 위해서는 기존의 Access Token의 유효기간을 짧게 하고 Refresh Token이라는 새로운 토큰을 발급합니다. 그렇게 된다면 Access Token을 탈취당해도 상대적으로 피해를 줄일 수 있습니다.
두 번째 단점은 Payload 정보를 제한적으로 담을 수 있습니다. Payload는 따로 암호화되지 않기 때문에 디코딩하면 누구나 정보를 확인할 수 있습니다. 세션을 활용하면 유저의 정보가 모두 서버 저장소에 보관되지만, 토큰은 그렇지 못합니다. 그렇기에 유저의 중요한 정보들은 Payload에 넣을 수 없습니다.
세 번째 단점은 세션/쿠키 방식에 비해 JWT는 HTTP를 통해 전송하기 때문에 페이로드의 크기가 클수록 데이터 전송에 있어서 비용이 커집니다. 따라서 인증이 필요한 요청이 많아질수록 서버의 자원낭비가 발생합니다.
즉, JWT를 활용하면, 토큰이 탈취당할 경우 보안에 취약하다는 문제가 발생할 수 있습니다. 그래서 토큰을 탈취당해도 유효기간을 줄이는 방법이 있지만, 그렇게 된다면, 사용자는 로그인을 자주 해서 토큰을 새롭게 발급받아야 하는 문제가 있습니다. 그럼 유효기간을 줄이면서도, 사용자가 로그인을 자주 하지 않도록 하는 방법을 고안해야 합니다. 이에 대한 답이 될 수 있는 것이 바로 "Refresh Token"입니다.
Refresh Token
Refresh Token은 Access Token과 똑같은 형태의 JWT입니다. 처음에 로그인을 완료했을 때 Access Token과 동시에 발급되는 Refresh Token은 긴 유효기간을 가지면서, Access Token이 유효기간이 다 됐을 때 새로 발급해주는 열쇠가 됩니다.
예를 들어 Refresh Token의 유효기간은 2주, Access Token의 유효기간은 1시간이라 한다면, 사용자는 API 요청을 하다가 1시간이 지나게 되면, 가지고 있는 Access Token은 만료됩니다. 그러면 Refresh Token의 유효기간 전까지는 Access Token을 새롭게 발급받을 수 있습니다. 물론 Refresh Token이 만료될 경우에는 사용자는 새로 로그인해야 합니다.
또한 Refresh Token 또한 탈취될 가능성이 있기 때문에 적절한 유효기간 설정을 보통 2주로 한다고 합니다. 그렇다면, Refresh Token을 적용했을 때의 토큰 적용 방식은 어떻게 될 수 있는지 알아보겠습니다.
Access Token만 활용했을 때보다 조금 더 복잡한 과정이 생깁니다. 이에 대해 알아보겠습니다.
1. 사용자가 ID , PW를 통해 로그인합니다.
2. 서버에서는 회원 DB에서 값을 비교합니다(보통 PW는 일반적으로 암호화해서 들어갑니다)
3~4. 로그인이 완료되면 Access Token, Refresh Token을 발급합니다. 이때 일반적으로 회원 DB에 Refresh Token을 저장해둡니다.
5. 사용자는 Refresh Token은 안전한 저장소에 저장 후, Access Token을 헤더에 실어 요청을 보냅니다.
6~7. Access Token을 검증하여 이에 맞는 데이터를 보냅니다.
8. 시간이 지나 Access Token이 만료됐다고 보겠습니다.
9. 사용자는 이전과 동일하게 Access Token을 헤더에 실어 요청을 보냅니다.
10~11. 서버는 Access Token이 만료됨을 확인하고 권한 없음을 신호로 보냅니다.
(Access Token 만료가 될 때마다 계속 과정 9~11을 거칠 필요는 없습니다. 사용자(프론트엔드)에서 Access Token의 Payload를 통해 유효기간을 알 수 있습니다. 따라서 프론트엔드 단에서 API 요청 전에 토큰이 만료됐다면 바로 재발급 요청을 할 수도 있습니다.)
12. 사용자는 Refresh Token과 Access Token을 함께 서버로 보냅니다.
13. 서버는 받은 Access Token이 조작되지 않았는지 확인한 후, Refresh Token과 사용자의 DB에 저장되어 있던 Refresh Token을 비교합니다. Token이 동일하고 유효기간도 지나지 않았다면 새로운 Access Token을 발급해줍니다.
14. 서버는 새로운 Access Token을 헤더에 실어 다시 API 요청을 진행합니다.
이런 RefreshToken을 활용하면 기존의 Access Token만 있을 때보다 안전하지만, 다소 구현이 복잡해집니다. 또한 Access Token이 만료될 때마다 새롭게 발급하는 과정에서 생기는 HTTP 요청 횟수가 많습니다. 이는 서버의 자원 낭비로 귀결됩니다.
마치며
지금까지 기본적인 JWT에 대한 내용을 공부해봤습니다. 하지만 JWT에 대해 궁금한 점이 더 많습니다. JWT를 어디에 저장해야 하는 것이며, JWT를 사용하면 중복 로그인을 막을 순 없는 것인지, JWT 토큰을 탈취당하기 전 후에는 어떻게 해야 하는 것인지 등을 더 알아보고 싶습니다.
출처
'Web' 카테고리의 다른 글
[Web] 토큰을 사용할 때 Bearer는 무엇인가? (0) | 2022.05.25 |
---|---|
[Web] 안전하게 로그인 처리하기 (0) | 2022.02.04 |
[Web] 다중 서버에서 세션을 관리해보자 - 4 (feat Redis, Memcached) (0) | 2022.01.11 |
[Web] 다중 서버에서 세션을 관리해보자 - 3 (feat 세션 스토리지 선택) (0) | 2022.01.08 |
[Web] 다중 서버에서 세션을 관리해보자 - 2 (feat 세션 불일치) (0) | 2022.01.05 |