여기서는 JWT(Json Web Token)에 대해서 다룰 것입니다.

해당 페이지([Security] 쿠키와 세션)에서 세션 로그인을 하는 방법에 대해서 설명 하였고, 그것이 가지고 있는 한계에 대해서 정리를 했습니다.

이것을 극복하기 위해서 Token 기반의 인증에 대해서 정리를 하겠습니다.

토큰

토큰 기반 인증 시스템은 클라이언트가 서버에 접속을 하면 서버에서 해당 클라이언트에게 인증되었다는 의미로 '토큰'을 부여합니다. 이 토큰은 유일하며 발급받은 클라이언트는 또 다시 요청을 보낼 때 '선택적으로' 요청 헤더에 토큰을 담아서 보냅니다.

인증이 필요한 요청의 경우, 해당 토큰을 파싱(parsing)하는 과정을 통해 사용자 인증 작업을 수행합니다.

이전에 봤던 세션기반 인증은 서버가 파일이나 데이터베이스에 세션정보를 가지고 있어야 하고 이를 조회하는 과정이 필요하기 때문에 많은 오버헤드가 발생합니다. 하지만 토큰은 세션과 달리 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수 있습니다.

토큰 자체에 데이터가 들어있기 때문에 클라이언트에서 받아 위조되었는지 판별만 하면 됩니다.

⚡️ 파일이나 데이터베이스 조회로 인한 오버헤드 ⚡️

세션 기반을 통해서 데이터를 저장하려면 서버에 저장을 해야한다고 정리를 했었습니다. 서버에 저장되기 위해서 특정 경로에 있는 파일에 해당 정보를 담아둘 것이고, 이것은 파일 시스템을 호출하는 "시스템 콜"에 의해서 수행될 것입니다.
만약, 확장성을 위해서 이것을 데이터베이스에 저장을 해서 사용을 한다면 데이터베이스에서 가져오는 작업을 요구하게 될 것입니다.
이러한 두 작업은 I/O 작업을 요구하며 I/O 작업의 경우 서버에서 수행하는 기능 중 거의 가장 느린 작업에 속하므로 이것은 속도적인 측면에서 좋지 않습니다.

토큰 기반 방식은 서버 기반 방식과 다르게 서버에 정보를 저장하지 않으므로 stateless한 것입니다.

동작의 순서는 다음과 같습니다.

  1. 사용자가 본인의 인증 정보를 입력하고 로그인을 시도합니다.
  2. 서버에서 검증이 완료되면 유일한 토큰을 할당해줍니다. 이 과정에서 서버는 아무런 정보도 저장하지 않습니다.
  3. 클라이언트는 서버로부터 받은 토큰을 스토리지 혹은 쿠키에 저장을 해두고, 필요한 경우 헤더 혹은 쿠키에 담아서 보냅니다.
  4. 서버는 토큰을 파싱하여 본인이 발급한 토큰이 맞는지 확인하여 사용자를 검증할 수 있습니다. 토큰에는 사용자를 식별하기 위한 유일한 값(PK 혹은 UK)가 담겨있스빈다.

토큰의 단점은 다음과 같습니다.

  1. 쿠키/세션과 다르게 토큰 자체의 데이터의 길이가 길어, 인증 요청이 많아질수록 네트워크 부하가 심해질 수 있습니다.
  2. payload 자체가 암호화되지 않기 때문에 중요한 정보를 담을 수 없습니다.
  3. 토큰을 탈취 당하면 대처하기 어렵습니다.

3번의 경우, 토큰의 기간을 짧게 가져가고, CSRF 토큰도 같이 사용하면서 문제를 해결할 수 있습니다.

JWT

JWT(JSON Web Token)은 웹 표준(RFC 7519)으로, 두 당사자 사이에서 정보를 JSON 객체로 안전하게 전송하기 위해 디자인한 컴팩트한 토큰입니다.

JWT는 JSON 데이터를 BASE64 URL-safe Encode를 통해 인코딩하여 직렬화 한 것으로, 토큰 내부에는 위변조 방지를 위한 전자 서명도 들어 있습니다.

Spring Boot 프로젝트에서 Spring Security를 이용하여 JWT를 발급하여 사용하려고 할 때, 암호화 알고리즘을 적는 매개변수 칸이 존재합니다. 이것은 페이로드를 암호화하려고 하는 것이 아니라는 점을 명확히 해둬야 합니다. 이것은 전자서명을 위해 사용됩니다.

구조

JWT 구조는 아래와 같습니다.

구분자로 .이 사용되고, Header.Payload.Signature 의 구조를 갖고 있습니다.

  1. 헤더(Header): 토큰의 유형(JWT)와 사용된 서명 암호화 알고리즘(예: HMAC SHA256 또는 RSA)을 지정합니다.
  2. 페이로드(Payload): 클레임(claim)을 포함합니다. 클레임은 실제 전달하고자 하는 데이터입니다. 이에는 사용자 식별 정보, 토큰 발행자, 만료 시간등이 포함될 수 있습니다.
  3. 서명(Signature): 토큰의 유효성을 검증하는 데 사용됩니다. 서버는 대칭키 혹은 비밀키를 사용해 서명을 생성합니다.

특징

JWT의 특징을 정리하자면 다음과 같습니다.

  1. 자가 포함(self-contained): JWT는 필요한 모든 자체 정보를 갖고 있어, 사용자 상태 정보를 저장할 필요가 없습니다. 이는 상태가 없는(stateless) 인증 매커지즘으로 만듭니다.
  2. 보안성: 서명을 통해 JWT의 데이터가 변조되지 않았음을 보장합니다.
  3. 확장성(Scalability) 및 효율성: JWT는 자가 포함되어 있기 때문에, 중앙 집중식 인증 서버의 부하를 줄여주고, 분산 시스템에서의 확장성을 높여줍니다.
  4. 만료 기간(exp): 페이로드에 만료 기간 클레임을 포함할 수 있어, 토큰의 유효 시간을 쉽게 관리할 수 있습니다.

서명의 경우, 대칭키 암호화 방식과 공개키 암호화 방식에서 선택할 수 있으며, 대칭키 방식은 공개키 방식에 비해서 동작 수행이 빠르지만 키를 공유하기 때문에 보안적으로 위험할 수 있습니다.
만일, 서명을 만드는 키가 탈취당할 경우 매우 위험해질 수 있습니다.

공개키 방식은 오직 "비밀키"만을 사용하여 암호화를 수행하고 공개키를 공유하면서 복호화 작업을 수행하기 때문에 보안상 더 안전합니다. 하지만, 대칭키 방식보다는 속도적인 측면에서 느립니다.

애플리케이션이 분산된 구조여서 사용자 검증이 여러 서버에서 이루어지는 상황이라면키를 공유해야 되는 상황이 원격으로 이루어 질 수 있습니다.

이러한 경우, 공개키 방식을 사용한다면 대칭키보다 훨씬 안전하게 키를 주고 받으면서도 보안을 챙겨갈 수 있습니다.

시그니처(서명)의 구조는 헤더페이로드와 서버가 가지고 있는 시크릿 키를 사용하여 암호화합니다.

신뢰성

유저 JWT: A(Header) + B(Payload) + C(Signature)일 때 누군가 토큰을 탈취하여 B를 수정했다고 해봅시다.

  1. 다른 유저가 B를 임의로 수정: A + B' + C
  2. 수정한 토큰을 서버에서 받아서 유효성 검증 수행
    • 유저 JWT: A + B' + C
    • 서버에서 검증후 생성한 JWT: A + B' + C'
  3. 대조 결과가 일치하지 않아 유저의 정보가 조작되었음을 알 수 있습니다.

앞서 말했지만, 토큰은 '정보 보호'의 목적을 위해 사용하기 보다 '신뢰성' 즉, 위조 방지를 위해서 사용합니다.

장단점

마지막으로 JWT의 장단점을 정리해보면 다음과 같습니다.

장점

  1. Header와 Payload를 가지고 Signature를 생성하므로 데이터의 위변조를 막을 수 있습니다.
  2. 인증 정보에 대한 별도의 저장소가 필요 없습니다.
  3. JWT는 토큰에 대한 기본 정보와 전달할 정보 및 토큰이 검증되었음을 증명하는 서명 등 필요한 모든 정보를 자체적으로 가지고 있습니다.(self-contained)
  4. 세션과 다르게, 서버는 무상태(stateless)가 되어 서버의 확장성에 영향을 주지 않습니다.
  5. 토큰 기반으로 다른 로그인 시스템에 접근 및 권한 공유가 가능합니다.(쿠키와 차이)
  6. 모바일 애플리케이션에서도 잘 동작한다.(모바일 세션은 사용 불가능)

2번, 3번과 4번으로 얻을 수 있는 가장 큰 이점은 인증 절차에서 DB 조회를 할 필요가 없다는 것입니다.
서버 자체가 죽는 경우도 있지만, 대부분 DB가 터져서 서버도 같이 죽는 경우가 많습니다.
이런 점에서, JWT에 사용자 고유 인증 정보(PK)와 권한을 담아서 보낸다면 이를 통해 많은 오버헤드를 줄일 수 있습니다.

단점

  1. self-contained: 토큰 자체에 정보를 담고 있기 때문에 반대로 위험할 수 있습니다.
  2. 토큰 길이: 토큰은 Payload에 3종류 클레임을 저장하기 때문에, 정보가 많아질수록 토큰의 길이가 늘어나 네트워크에 부하를 줄 수 있습니다.
  3. Payload 인코딩: payload 자체는 암호화된 것이 아니라 BASE64로 인코딩된 것이기 때문에, payload에 중요 데이터를 넣으면 안됩니다.
  4. Store Token: stateless 특징을 가지기 때문에, 토큰을 클라이언트 측에서 관리하고 저장한다. 때문에 토큰 자체를 탈취당하면 대처하기 어렵게 됩니다.

4번의 단점으로 인해서 토큰 자체가 탈취당한 경우, 다른 공격자가 해당 토큰을 가지고 다른 사람 흉내를 내서 각종 작업을 수행할 수 있습니다.

이것은 매우 위험한 상황을 유발할 수 있으므로 무조건 피해야 합니다. 이 문제를 해결하기 위해서 토큰이 탈취당하더라고 사용하지 못하도록 막을 수 있어야 합니다.

따라서 토큰의 만료 기간을 짧게 가져가서 토큰을 탈취하더라도 만료 기간이 다되도록 하여 해당 문제를 해결할 수 있습니다.

하지만, 또 다른 문제가 생기는데 만료 기간을 짧게 가져갈 경우에 사용자가 만료 기간보다 더 오랫동안 해당 토큰을 애플리케이션에서 사용하려고 하는 경우 토큰이 도중에 만료가 될 것입니다.

토큰이 만료되었기 때문에 사용자는 토큰을 재발급받아야 하고, 위에서 봤던 로직대로라면 로그인 절차를 다시 수행해야 합니다. 이것은 사용자에게 불편함을 제공하므로 보안만큼이나 큰 문제입니다.

여기서 Refresh Token의 개념이 등장합니다.

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 TokenRefresh Token을 발급 받습니다.

이후 사용자가 사용하고 있던 Access Token을 가지고 검증을 수행하다가 만료기간이 지난 경우 재발급 로직을 수행해야 합니다.

재발급 로직은 저장되어 있는 Refresh Token과 사용자가 보내온 Refresh Token을 비교하여 일치한 경우, 새로 Refresh Token으로 업데이트 해주고 Access Token도 새롭게 재발급하여 두 토큰을 사용자에게 보냅니다.

사용자는 별도의 재로그인 과정을 수행할 필요 없이 계속해서 서비스를 이용할 수 있습니다.

Refresh Token 검증 케이스

Refresh Token은 어떠한 경우에 사용될지 상황을 분석해 봅시다.
(해당 상황은 Access Token과 Refresh Token을 사용하는 경우에 발생하는 CASE로 설계시 고려해야 합니다.)

  1. Access Token과 Refresh Token이 전부 유효한 경우
  2. Access Token은 만료되었지만, Refresh Token은 유효한 경우
  3. Access Token는 유효하지만, Refresh Token은 만료된 경우
  4. Access Token과 Refresh Token 모두 만료된 경우

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번은 두 토큰 다 유효하지 않으므로 사용자가 로그인을 통해 새롭게 재발급 받아야 합니다.

전체 인증 절차

인증 과정은 아래의 사진을 통해 확인할 수 있습니다.

별다른 설명을 적을 필요가 없어 보이므로 넘어가겠습니다.

Refresh Token Rotation

해당 기법은 다음과 같은 문제 상황을 해결하기 위해 사용하는 방법입니다.

만약, 공격자가 만료된 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 관계를 가지고 있는데,

  1. Refresh Token Rotaion을 적용하지 않는다면,
    a. Refresh Token의 업데이트보다 Access Token의 업데이트가 더 자주 일어납니다.
    b. Access Token의 업데이트 과정에서는 Refresh Token의 업데이트 과정을 수행하지 않으면서 더 빠른 속도로 재발급 로직을 수행할 수 있습니다.
    c. 보안 취약점을 가지고 있습니다.
  2. Refresh Token Rotaion을 적용한다면,
    a. Access Token을 업데이트 할 때마다 Refresh Token 또한 업데이트를 해줘야 합니다.
    b. Refresh Token이 탈취되더라도 새롭게 업데이트 되어있을 확률이 높기 때문에 보안적인 측면에서 다소 안전합니다.

보안 - 저장 위치

여기서는 본격적으로 보안적인 측면을 고려하여 Access Token과 Refresh Token을 어디에 저장해야 될지 고려를 해봅시다.
(여기서부터 필기는 필자의 고민을 바탕으로 작성된 내용이 많습니다.)

먼저, 두 토큰의 저장 가능한 위치를 적어봅시다.

  1. 로컬 스토리지(브라우저)
  2. 쿠키
  3. HTTP Only 쿠키

여기서는 통신 과정에서 SSL/TLS 암호화 프로토콜을 적용한 것으로 예를 들겠습니다.

사실, 1번과 2번을 사용하는 것은 보안상 매우 취약해질 수 있습니다. HTTPS 방식을 사용하기 때문에 패킷 탈취는 힘들다고 하지만, XSS(Cross Site Script) 방식을 통해서 로컬 스토리지와 쿠키에 접근할 수 있기 때문에, 보안상 위험합니다.

하지만, 3번을 사용할 경우 스크립트를 사용한 접근은 불가능합니다. 하지만, 쿠키는 자동 전송 기능이 있기 때문에 이것으로 인해서 CSRF(Cross Site Request Forgery)를 통해 악용될 가능성이 존재합니다.

이 문제까지 해결하기 위해서 CSRF Token까지 고려를 한다면 문제가 다소 해결될 수 있지만 현재 페이지에서는 다루지 않을 것입니다.

최종적으로 Access TokenRefresh Token을 사용한 경우, 가장 안전한 경우는 3번의 방식인 HTTP Only 설정을 걸어둔 쿠키에 저장하는 것이 가장 안전합니다.

참고한 자료

profile
개발정리블로그

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN