본문 바로가기

Study/개발일지

[백엔드온라인TIL] JWT의 Refresh Token과 Access Token은 어디에 저장해야 할까? 그리고 successfulAuthentication (62일차)

JWT에서 가장 중요한 점은 '보안'입니다.

어떤 공격에도 토큰을 탈취당하면 안되고 어떤 방식으로도 악용당하면 안되기 때문에 이 토큰을 어디에 어떻게 저장할 지를 고민해 보았습니다.

 

1. Access Token

먼저 Access Token은 말 그대로 인증 인가 서비스를 구현하기 위한 것입니다.

  • 사용자가 로그인을 하면 백엔드 서버에서 토큰을 만들어서 헤더에 담아 클라이언트에 전송합니다.
  • 클라이언트는 서버에 요청을 할 때 서버로부터 받았던 토큰을 함께 전송합니다.
  • 서버는 요청과 토큰을 함께 받고 서버에서 토큰을 디코딩하여 로그인한 사용자를 확인합니다.

Access token은 서버에서 사용자를 로그아웃시키는 권한을 가질 수가 없기 때문에 일정 기간이 지나면 사용자가 자동으로 로그아웃되도록 만료 기간을 짧게 설정합니다. 그렇지 않으면 한 번 외부에서 로그인하였을 시 수동으로 로그아웃하지 않으면 후에 다른 누군가가 계정에 접근할 수 있기 때문입니다.

또 토큰 기반 인증방식에서 토큰은 Stateless합니다. 서버가 토큰의 상태를 보관하지 않고 있기 때문에 한 번 발급한 토큰에 대해서 제어권을 가지고 있지 않다는 뜻입니다. 이런 토큰이 탈취당한다면 사용자는 토큰이 만료되기 전까지 계정의 제어권을 가진 해커에게 모든 정보를 속수무책으로 넘겨주게 됩니다.

 

2. Local Stroage, Session Storage, Cookie

그렇다면 이 Access token은 어디에 저장해야 안전할까요? 웹 프로젝트를 진행할 때는 로컬 스토리지, 세션 스토리지, 쿠키에 저장하는 것이 일반적입니다.

 

로컬 스토리지와 세션 스토리지의 가장 큰 차이점은 '데이터의 영구성'입니다. 로컬 스토리지에 저장된 데이터는 사용자가 지우지 않는 이상 계속 브라우저에 남아 있지만, 세션 스토리지의 데이터는 윈도우나 브라우저 탭을 닫을 경우에는 제거됩니다. 따라서, 지속적으로 필요한 데이터(예: 자동 로그인)는 로컬 스토리지에 저장하고, 잠깐 필요한 정보(예: 일회성 로그인)는 세션 스토리지에 저장하는 것이 일반적입니다.

 

하지만 이 두 스토리지는 치명적인 단점이 있는데, 바로 XSS(Cross Site Scripting) 공격에 취약하다는 것입니다. XSS 공격은 공격자가 상대방의 브라우저에 악성 스크립트를 삽입하여 사용자의 세션을 탈취하거나 웹사이트를 변조하거나, 악의적인 동작을 수행하는 보안 공격입니다. 두 스토리지는 모두 자바스크립트로 데이터를 저장하고 꺼낼 수 있기 때문에 XSS 공격에 취약합니다. 따라서, 스토리지에 민감한 정보를 저장하는 것은 안전하지 않습니다.

 

그 다음으로 브라우저의 저장소 역할을 하는 쿠키가 있습니다. 쿠키는 만료 기한이 있는 키-값 저장소입니다. 쿠키 또한 자바스크립트로 접근이 가능하지만, HTTP ONLY 옵션을 설정하여 자바스크립트로 접근하는 것을 방지할 수 있습니다. 하지만 쿠키에 토큰을 담으면 CSRF(Cross-Site Request Forgery) 공격에 취약해집니다. XSS 공격은 토큰의 값을 가져오지만, CSRF 공격은 로그인된 상태에서 특정 동작을 요청하게 만듭니다. 이는 CSRF 방어를 실시하여 어느 정도 상쇄할 수 있습니다.

즉, CSRF는 쿠키에 저장된 토큰의 값을 직접적으로 가져오지는 않기 때문에 쿠키에 토큰을 저장하는 것이 합리적입니다.

 

3. Refresh Token

Refresh Token은 Access Token의 유효 기간을 짧게 하여 보안을 강화하면서도 사용자가 자주 로그아웃되지 않도록 하는 목적으로 등장했습니다.

 

Refresh Token을 어디에 저장하는 것이 효율적이고 안전한지에 대해 가장 좋은 방법은 DB에 저장하는 것입니다.

 

Refresh Token 값을 DB에 저장하고, 클라이언트는 인덱스 값을 쿠키나 로컬 스토리지에 저장합니다. 쿠키에 저장하는 경우, 만료 기간을 길게 잡으면 충분히 끊임없이 로그인된 상태를 유지할 수 있습니다. 이렇게 하면 Refresh Token의 값을 노출시키지 않고 인덱스 값만 클라이언트에 노출되므로 보안상 더 안전하게 저장할 수 있습니다. 더 나아가 인덱스 값도 사용자의 아이디나 추가 값을 조합하여 해시로 생성하여 사용하면 보안 측면에서 더 유리합니다.

 

그러나 만약 Refresh Token이 탈취된다면 대비하는 방법으로 RTR(Refresh Token Rotation)이 존재합니다.

 

RTR은 Refresh Token을 한 번만 사용할 수 있도록 만드는 것입니다. Refresh Token을 사용하여 새로운 Access Token을 발급받을 때마다 Refresh Token도 새롭게 발급받는 것이죠. 이렇게 하면 이미 사용된 Refresh Token을 검사하여 서비스 측에서 탈취를 확인할 수도 있습니다.

 

따라서, Refresh Token은 보안적인 측면을 고려하여 DB에 저장하고, RTR과 같은 방법을 적용하여 안전하게 관리하는 것이 좋습니다.

 

 

successfulAuthentication과 AuthenticationSuccessHandler

 

 

 혼자 시큐리티로 JWT를 발급하는 것을 공부해보며, successfulAuthentication가 아닌

AuthenticationSuccessHandler를 상속받아 구현하여, 이곳에서 JWT를 발급받게 해주었습니다.

successfulAuthentication에서 발급해주는 것과 AuthenticationSuccessHandler에서 발급하여 주는것의 차이가 있을까요?

혹시 별 차이가 없다면 어떠한 방법을 더 선호하시나요?

 

 

AuthenticationSuccessHandler를 통해 로그인 성공 시 어떤 프로세스로 진행될 것인지, 로그인 성공시 처리할 로직(이동 될 페이지 지정 등)에 대해 구현할 수 있습니다.

말씀하신 것처럼 2가지 모두 로그인 성공 후에 JWT를 발행 할수 있습니다. 간단하게 토큰만 발행해서 Response에 포함 시키는 경우라면 successfulAuthentication를 구현하는게 더 간단하며, 로그인에 대한 성공, 실패 및 사용자 정의 프로세스가 필요한 경우라면 AuthenticationSuccessHandler를 구현하는 게 좋을 듯 합니다.

선호도에 대한 부분보다는 작업에 필요한 내용으로 선택하면 좋을 것 같습니다. 

728x90