JWT 어디에 저장하지?

원정·1일 전
3
post-thumbnail

💰 발단


지난 포스팅에서 JWT을 이용한 로그인에 대해서 알아보고 개인 프로젝트에 적용했다.
로그인이 성공하면,

  • 리프래시 토큰은 서버에서 HttpOnly 쿠키에 저장한다.
  • 엑세스 토큰은 axios 전역 헤더에 저장한다.

이후의 요청에 대해서 토큰이 없거나, 토큰이 만료되면 서버로부터 401 에러를 반환받는다.
401 에러가 발생하면 토큰을 재발급하는 인터셉터를 만들었다.
추가로 로그인된 사용자가 로그인 화면에 접근하지 못하게 라우트 가드를 만들었다.

하지만 새로고침을 하게 되면 토큰이 날아가는 문제가 생겼다...!
엑세스 토큰이 날아가면서 로그인 상태가 유지되지 않는 것이다!

HTTP 헤더에 저장하게 되면 만사가 형통할 줄 알았는데, 해당 값은 자바스크립트 메모리에 저장되어 새로고침을 하게 되면 날아간다는 사실을 알았다.

인터셉터를 만들었음에도 왜 새로고침 시에 토큰을 다시 가져오지 못하는 걸까?

이유는 간단했다.
새로고침을 해도 서버로 API 요청을 보내지 않기 때문이다.
서버로 요청을 보내야 토큰을 검사해서 401 에러를 반환하고 재발급 과정이 이뤄지는데 새로고침만으로는 서버로 요청을 보내지 않는다.

어떻게 해결할 수 있을까?
내가 생각한 해결 방향은 세 가지였다.
1. 라우트 가드에서 useEffect를 사용해서 컴포넌트가 마운트 됐을 때 서버에 요청을 보낸다.
2. 라우트 가드에서 로그인 유무를 판별하는 다른 기준을 만든다.
3. 서버로부터 받은 엑세스 토큰을 로컬 스토리지에 저장한다.

💵 해결책 1: 라우트 가드에서 useEffect 활용

라우트 가드에 useEffect를 사용해서 컴포넌트가 마운트 됐을 때 서버로 사용자 정보를 요청한다.
서버에서는 리프래시 토큰을 확인하고 엑세스 토큰을 재발급한다.

useEffect(() => {
	fetchUserData(); // 서버에 사용자 정보 요청
}, [])

하지만 '내가 만들려는 프로젝트는 로그인 기반이기 때문에 모든 페이지마다 요청을 보내는 게 좋을까?'라는 의문이 생겼다.

벼룩을 잡으려고 초가삼간을 태우는 느낌이랄까...

💵 해결책2: 라우트 가드에서 로그인 유무를 판별하는 다른 기준을 만든다.

라우트 가드에서 토큰 유무가 아니라 다른 방법으로 로그인 유무를 확인하면 어떨까?

생각은 그럴싸했지만 어쨌든 토큰이 아닌 어떤 정보를 기준으로 판별하게 될텐데, 그 정보는 어디에 담지?
어차피 똑같은 짓이구나?!

💵 해결책3: 로컬 스토리지에 저장한다.

스토리지에 저장하는 건 피하고 싶었다.
로컬 스토리지에 저장하자니 왠지 모를 거부감이 들었기 때문이다.
아무래도 개발자 도구를 켜서 손쉽게 삭제할 수 있기 때문인가?!

돌이켜보면 왠지 모를 거부감도 왠지 모르게 거부감이 든다.
뭐가 나쁜지도 모르면서 거부감만 표하는 것 같아서다.

이번 기회에 토큰 저장 위치에 따른 장단점을 정리해야겠다고 생각했다.

💰 토큰 탈취 방법


지피지기 백전백승!
토큰을 탈취하는 방법을 알아야 지키는 방법도 알 수 있지 않을까?!

💵 XSS(Cross-Site-Scripting)

XSS는 공격자가 웹사이트에 악의적인 스크립트를 넣어 방문자의 브라우저에서 실행되도록 유도하는 방법이다.
이를 통해 사용자의 쿠키, 세션 토큰, 개인정보 등을 탈취할 수 있다.

XSS 공격은 크게 세 가지 유형이 있다.

1. 저장형 XSS (Stored XSS)

공격자가 악의적인 스크립트를 웹사이트의 데이터베이스(게시글, 댓글) 등에 저장하고, 다른 사용자가 이를 조회할 때 스크립트가 실행되는 방식이다.

예를 들어, 사용자가 블로그에 댓글을 작성할 때, 입력값을 검증하거나 이스케이프 처리하지 않고 그대로 데이터베이스에 저장한다면, 공격자는 <script>alert("XSS")</script>와 같은 스크립트를 댓글에 입력할 수 있다.

이 댓글을 읽는 다른 사용자의 브라우저에서는 저장된 스크립트가 실행되어 공격자가 의도한 동작이 발생한다.

2. 반사형 XSS (Reflected XSS)

공격자가 조작한 데이터를 웹 서버에 전송하면, 서버가 그 데이터를 바로 응답에 포함시켜 사용자에게 전달하는 경우 발생한다.
주로 검색어, 에러 메세지, URL 파라미터 등에서 나타난다.

예를 들어, 검색 기능이 있는 웹사이트에서 사용자가 입력한 검색어를 별도의 검증 없이 결과 페이지에 출력한다고 했을 때, 공격자가 URL에 https://example.com/search?q=<script>alert('XSS')</script>와 같이 스크립트를 포함시켜 링크를 만들어 이메일이나 메세지로 배포할 수 있다.

사용자가 해당 URL을 클릭하면, 서버는 사용자의 입력값을 그대로 결과 페이지에 출력하고, 브라우저는 악의적인 스크립트를 실행하게 된다.

3. DOM 기반 XSS (DOM-based XSS)

DOM 기반 XSS는 클라이언트 측 자바스크립트 코드에서 사용자 입력(URL의 해시, 쿼리스트링 등)을 안전하게 처리하지 않을 때 발생한다.
이 경우 서버는 문제가 없으며, 브라우저 내에서 DOM 조작 과정에서 취약점이 발생한다.

예를 들어, 자바스크립트로 URL의 해시 값을 읽어 페이지 일부에 동적으로 삽입하는 경우에 URL이 https://example.com/#<img src=x onerror=alert("XSS")>와 같이 구성되어 있다면, 클라이언트 측 스크립트가 이 해시 값을 DOM에 그대로 삽입할 경우, 브라우저는 삽입된 img 태그의 onerror 이벤트를 실행해 스크립트를 실행할 수 있다.

얼마 전, 같이 항해를 했던 팀원분께서 해당 내용과 관련한 영상)을 보여준 적이 있다.
스크립트 태그로 DOM 조작이 가능하니, CNN 사이트에서 카지노 사이트를 홍보하는 기사를 넣는 식의 내용이 있었다.
XSS로 정보를 탈취하는 것뿐만 아니라 가짜 뉴스를 만든다거나 할 수도 있다는 뜻이다.

방지 방법

XSS 공격에 대응하기 위해서는

  • 사용자로 부터 입력받은 데이터를 반드시 신뢰할 수 없는 값으로 간주하고 검증 및 필터링하는 방법.
  • 사용자 입력을 HTML에 출력할 때 반드시 이스케이프 처리를 하여 코드로 인식하지 않고 단순 텍스트로 표시하도록 한다.
    프론트엔드에서 주로 사용하는 리액트, 앵귤러, 뷰 등에서는 기본적으로 XSS 공격에 대해 어느 정도의 보호를 해준다고 한다.
    리액트를 처음 공부할 때는 선언형으로 바꿔줘 개발할 때 편리하게 해주는 도구 정도로 생각했는데, 알면 알 수록 여러 귀찮은 일들을 대신 해주는 구나 싶다.

추가로 CSP(Content Security Policy)가 있다.
CSP는 웹사이트에서 실행 가능한 리소스(스크립트, 스타일, 이미지 등)의 출처를 명시적으로 제한하여, 악의적인 콘텐츠가 실행되는 것을 방지한다.

아무튼 토큰 관점에서 보면 HttpOnly 쿠키를 사용한다.
HttpOnly 쿠키는 웹 애플리케이션에서 민감한 정보를 저장할 때 사용하는 쿠키의 한 종류로 클라이언트 측 스크립트에서 접근할 수 없도록 설정된 쿠키다.

스크립트에서 접근할 수는 없지만, 브라우저가 서버로 HTTP 요청을 보낼 때만 자동으로 전송된다.
따라서 클라이언트 측에서 쿠키 값을 기반으로 하는 로직(로그인 유무 검사)를 구현할 때는 별도의 API를 통해 서버에서 인증 정보를 받아와야 한다.

HTTPS를 사용하지 않으면 쿠키의 전송 도중에 도청 위험이 있기 때문에 secure 옵션을 함께 사용하는 것이 좋다.
secure 옵션은 쿠키를 HTTPS와 같은 안전한 연결에서만 전송하도록 제한하는 속성이다.

리프래쉬 토큰을 HttpOnly 저장하는 이유가 XSS 공격을 방지하기 위해서다.
클라이언트에서 사용하지 않으면서, 엑세스 토큰 재발급을 위해 서버에서 사용되므로.

💵 CSRF(Cross-Site Request Forgery)

CSRF는 사용자가 의도하지 않은 요청을 악의적인 사이트를 통해 다른 사이트에 보내도록 만드는 공격 방식이다.
이를 통해 공격자가 피해자의 브라우저를 이용해 피해자가 이미 로그인한 사이트에 원하지 않는 요청(계좌 이체, 비밀번호 변경 등)을 실행시키게 만든다.

CSRF 동작 방식

  • 사용자 인증: 피해자가 신뢰할 수 있는 사이트(은행 등)에 로그인하면, 브라우저에는 인증 정보를 담은 쿠키가 저장된다.
  • 악의적인 사이트 방문: 피해자가 악의적인 사이트를 방문하면, 이 사이트에 포함된 스크립트나 링크를 통해 자동으로 요청이 전송된다.
  • 쿠키 자동 전송: 브라우저는 동일한 도메인에 대한 요청 시 자동으로 인증 쿠키를 포함하므로, 피해자의 인증 정보가 포함된 요청을 보낸다.

방지 방법

CSRF 공격에 대응하기 위해서는

  • CSRF 토큰을 발급하여 쿠키가 아닌 클라리언트에서 관리하도록 한다.
  • SameSite 쿠키 속성을 Strict또는 Lax로 설정하면, 타 도메인에서 오는 요청에 쿠키가 자동 전송되지 않도록 할 수 있다.
  • 요청 헤더의 RefererOrigin을 검사하여, 요청이 신뢰할 수 있는 출처에서 온 건지 확인할 수 있다.
  • 민감한 동작 전에 OTP, 2차 인증 등을 요구한다.

💰 개인적인 결론


CSRF에 대해서 알게 되니 엑세스 토큰은 로컬 스토리지에 저장하는 것이 좋다고 생각했다.
CSRF 공격을 받았을 때 쿠키에 엑세스 토큰과 리프래쉬 토큰이 모두 들어있다면 모든 요청이 가능해지기 때문이다.
인피니티 스톤이 한 곳에 있었으면 어벤져스가 결성되기도 전에 인구의 반이 사라질 수도 있으니까?!

애초에 JWT가 탈취됐을 경우 위험을 최소화하기 위해 엑세스 토큰과 리프래쉬 토큰으로 나눈 건데 모두 쿠키에 저장한다면 자체가 둘로 나눈 의미를 무색하게 만드는 게 아닌가 생각한다.

아무튼 새로고침 시에 로그인이 유지가 되지 않는 문제는 라우트 가드에서 로컬 스토리지에 저장된 토큰의 유무에 따라 판단하도록 로직을 수정했다.

0개의 댓글

관련 채용 정보