[Web] JWT토큰(Access token, Refresh Token) 전달 방식, 저장 위치

2025. 12. 30. 20:32·개발 공부/Web
728x90
반응형

내가 하고 있는 서비스는 SSO로그인만 지원하지만, SSO 로그인이 원활하지 않아서 테스트 할 때는 Token기반의 로그인을 사용하고 있다. 내가 처음부터 구현한 서비스가 아니라서 예전에 다른 사람이 구현해놓은 코드를 이번에 개선하게 되면서 공부한 내용을 정리해보려고 한다.

🧐로그인 방식에는 어떤 방식이 있을까

세션 방식

sequenceDiagram
    participant c as Client
    participant s as Server
    participant d as DB

    c->>s: 로그인 요청
    s->>s: 세션 id 생성
    s->>d: 세션 id와 유저 정보 저장
    s->>c: 세션 id 전달
    c->>s: 요청할 때 세션 id를 담아서 요청
  1. Client 가 서버에 로그인 요청
  2. 정상적인 요청인 경우, session id를 만들고, DB에 session id와 유저 정보를 저장
  3. Client에게 session id 전달
  4. 이후 client는 요청할 때마다 session id를 담아서 요청
  5. 서버는 session id를 가지고 유저를 구별

특징

  • 사용자의 정보를 서버에서만 관리, Client는 오직 session Id만 저장
  • 사용자 정보를 모두 서버에서 관리하기 때문에 외부에 노출되지 않지만, 세션 ID가 탈취당하면 무의미해짐
  • 사용자가 많을 수록 DB에서 관리해야하는 세션의 수가 많아져 부하 가중

 

💳JWT(Json Web Token) 방식

  • JWT는 사용자를 인증하고, 식별하기 위한 토큰 기반 인증 방식
  • 토큰 자체에 사용자에 대한 일부 정보가 포함되어 있기 때문에 민감한 정보는 포함되면 안됨.
  • 사용자에 대한 정보가 토큰에 담겨있고, 클라이언트에서 관리하기 때문에 DB 부하가 발생할 가능성이 낮음.
  • 보안과 사용성을 위해서 Access token/Refresh token으로 나뉨

Access token

  • API호출 할 때 실제로 사용하는 token
  • 탈취되면 위험하기 때문에 보통 유효기간이 짧음

Refresh token

  • 오직 Access token을 재발급하기 위한 token
  • 상대적으로 유효기간이 길다

 

❔어디에 저장해야 할까?

이제 토큰을 어디에 저장해야할 지 생각해봐야한다. 토큰이 탈취당하면 악용될 수 있기 때문이다.

어디에 저장할지 정하기 전에, 먼저 어떤 위험들이 있는지에 대해 알아보자. 그래야지 어떻게 막을지 생각할 수 있기 때문이다.

취약점

XSS

발생 조건

  • 토큰을 JS로 접근할 수 있는 공간에 저장(로컬 스토리지, Redux, JS 변수에 저장)
  • XSS 취약점 존재 (=JS로 접근이 가능)

발생 과정

  1. XSS 취약점이 존재하는 곳에 악성 스크립트를 실행시켜 token값을 탈취
  2. 탈취한 token 값을 사용해 API 호출

CSRF

발생조건

  • 브라우저가 자동으로 서버에게 쿠키 전송
  • CSRF 방어 안되어있음.

발생 과정

  1. 사용자가 로그인 상태로 악성 사이트 방문
  2. 악성 사이트에서 우리 서버를 호출하는 코드를 심어서 사용자가 그 코드를 실행하게 되면(이미지 클릭이나 게시글 클릭과 같은 액션에 숨겨놓음), 마치 사용자가 우리 서버를 호출 하는 것처럼 할 수 있음
  3. 우리 서버에서는 사용자가 호출하는 것처럼 보이기 때문에 요청을 수락하게 됨

크게 정리하면, JS로 접근할 수 없는 곳이여야하고, 로그인한 사용자가 의도한 요청인지 확인을 해주어야 이 취약점들을 막을 수 있다.

 

저장 위치

로컬

로컬 스토리지/세션 스토리지

  • 프론트에서 localStorage.setItem/getItem 함수를 통해 저장/조회할 수 있다.
  • 누구나 확인할 수 있기 때문에 (개발자 도구 > Application) 노출될 가능성이 매우 높다
  • 그렇기 때문에 XSS 공격에 취약하다

쿠키

  • 자동으로 서버에 전송된다.
  • 그렇기 때문에 CSRF 공격에 노출되면 자동으로 서버에 전송되어 인증이 되기 때문에 취약하다.
  • 그러나 몇가지 옵션으로 방어할 수 있다.
    • HttpOnly = 적용
      • JS로 접근 불가 (document.cookie 불가능)
      • XSS를 방지할 수 있지만, CSRF는 막을 수 없음
    • Samesite
      • 다른 사이트에서 오는 요청은 쿠키 보내지 않음
      • CSRF 방어 가능
    • Secure
      • HTTPS 요청에서만 쿠키 전송
      • HTTP인 경우에는 전송되지 않음
        • 중간자 공격 방지(MITM)
        • 공용 와이파이, 프록시 환경에서 쿠키 탈취 방지

Redux?

기존에 구현된 코드에서는 AccessToken을 Redux에서도 관리하고 있었다. 그래서 redux에서 관리하는건 괜찮을까 싶어서 찾아봤는데,,

결론은 보안에 취약하다

  • 리덕스도 JS를 사용해서 접근할 수 있기 때문에 XSS 에 노출될 수 있다.
  • 또한 새로고침을 해도 redux 상태를 유지하기 위해 redux persist를 사용하는데, 이렇게 되면 사실상 localStorage에 저장하는 것과 동일

하다고 한다.

 

 

✅ 내가 선택한 방식

공통 적용 사항(Silent Refresh )

->  401 만료 되면 자동으로 /refresh 호출해서 새로운 access token과 refresh token을 발급해서 전달

 

Refresh 호출 타이밍

고민 사항🤔

  1. 요청전에 access 토큰의 여부나 만료일을 확인해서 유효하지 않다면, 그 때 /refresh 호출
    → 클라이언트 시간에 의존하기 때문에 서버와 시간이 다르면 오판할 수 있음
  2. 요청하고 나서 401을 받게 되면 그 때서야 /refresh 호출 (선택✅)
    → 제일 많이 사용하는 방식, 토큰 검증을 서버에서만 하기 때문에 명확한 역할 분리
  3. 아니면 발급 받고 나서 setTimeout 을 사용해서 만료되기 1분전에 재발급 요청하기
    → 이것 또한 timeout이 정확하다고 할 수 없음. 시간이 틀어질 수 있는 위험 존재

 

 

Access token 저장 위치

후보1 - 웹 내 메모리, private 변수

  • 클로저 내부나 특정 모듈의 프라이빗 변수에 담겨 있다면, 변수 이름을 알아내어 접근하기 어려움
    • closer 예시
    // tokenStore.js
    export const tokenStore = (() => {
      let accessToken = null; // 🔒 완전 프라이빗
    
      return {
        set(token) {
          accessToken = token;
        },
        get() {
          return accessToken;
        },
        clear() {
          accessToken = null;
        },
      };
    })();
  • 사용자가 새로고침을 하게 되면 변수의 값이 사라지기 때문에 /refresh 요청하는 횟수가 많아짐

 

후보2 - local storage

  • 새로고침을 해도 사라지지 않기 때문에 /refresh 횟수가 상대적으로 적음
  • XSS 공격에 매우매우 취약함.

 

후보3 - cookie + http only

→ 보안적으로 제일 안전하나, 기존 코드가 모두 헤더에 Authorization 값에 토큰을 넣어서 전달하는 방식으로 구현되어있어서 코드 변경 범위가 커짐

 

선택

→ 클로저 방식으로 메모리에 저장하는 것이 적합하다고 판단.

XSS공격을 막을 수는 없지만, XSS 피해 반경을 줄일 수 있음

더보기
  • Access token도 cookie로 보내는 건?우리 서비스도 Same site여서 cookie를 사용해도 되지만, 기존에 구현된 방식이 Bearer 로 구현되었기 때문에 한번에 너무 많은 부분을 변경하는 건 리스크가 크다고 생각하여 이부분은 그대로 유지하는 걸로 선택했다.
  • 사실 프론트와 서버의 주소가 Same site이면 Access token과 refresh token 모두 cookie를 사용해서 보내도 괜찮다.

 

Refresh token 저장 위치

→ 탈취 당하지 않도록 쿠키에 저장하고 여러가지 옵션을 설정하기로 결정

  • 쿠키를 사용하기 위해서는 withCredential 설정 필요 (FE, BE 둘 다 해당)
  • cookie의 http only, secure 설정이 필요
    • 왜?
      • refresh 토큰은 상대적으로 유효기간이 길고, access token을 발급할 수 있는 key이기 때문에 XSS를 막기 위해 cookie + http only를 사용(JS로 접근할 수 없음)
      • CSRF 공격을 막기 위해 sameSite(=Strict) 설정
      • secure 설정으로 https 에서만 쿠키가 전송되도록 설정
    → Samsite와 Secure을 설정하면 로컬에서 테스트가 어려움. 서버에서 개발 환경에 따라서 설정값을 분기 처리 해주는 걸로 해결할 수 있음.

    환경에 따라 쿠키 설정하는 코드 🔽
const isProd = process.env.NODE_ENV === 'production';

res.cookie('refreshToken', token, {
  httpOnly: true,
  secure: isProd,                
  sameSite: isProd ? 'strict' : 'lax',
  path: '/refresh',
  maxAge: 7 * 24 * 60 * 60 * 1000,
});

 


우선 우리 서비스는 Prod에서는 사용하지 않기 때문에 http only 옵션만 적용하기!
-> 하지만 이 옵션은 곧 변경되었다....

 

최종 결론

  1. Access token은 메모리 변수 관리하고, refrsh token은 쿠키로 관리한다.
  2. refresh token을 위해 서버에서 쿠키 설정을 해주어야 한다.
  3. Access token이 만료되면 클라이언트에서 알아서 /refresh를 호출해서 access token을 갱신한다.
  4. /refresh 호출을 했는데 refresh token이 만료되어 401 응답을 받으면 사용자는 로그아웃 된다.
728x90
반응형
저작자표시 비영리 동일조건 (새창열림)

'개발 공부 > Web' 카테고리의 다른 글

[Web] WebRTC  (0) 2022.07.29
[Web]SocketIO  (0) 2022.07.27
[Web] WebSocket 정리  (0) 2022.07.26
라디오 버튼을 이미지로 구현하기 [이전 블로그 게시글]  (0) 2022.02.16
[정리] 웹 폰트란? [이전 블로그 게시글]  (0) 2022.02.16
'개발 공부/Web' 카테고리의 다른 글
  • [Web] WebRTC
  • [Web]SocketIO
  • [Web] WebSocket 정리
  • 라디오 버튼을 이미지로 구현하기 [이전 블로그 게시글]
9_yoon
9_yoon
배울게 넘쳐나는 개발 세상에서 묵묵히 걸어가며 지식을 쌓는 신입 개발자
  • 9_yoon
    개발저장소
    9_yoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (104)
      • 알고리즘 (52)
        • BJ (40)
        • 프로그래머스 (0)
        • SWEA (10)
        • JO (2)
      • 이론 공부 (8)
        • 네트워크 (2)
        • 알고리즘 (2)
        • Java (1)
        • Web (2)
        • 기타 (1)
      • 개발 공부 (36)
        • Project (1)
        • JavaScript (1)
        • Typescript (1)
        • Spring (12)
        • Java (2)
        • Next JS (7)
        • React (3)
        • Vue (1)
        • Web (6)
        • 기타 (2)
      • 기타 (8)
        • SSAFY (7)
        • 일상 (0)
        • 회고 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    김영한 인프런
    spring
    스프링
    싸피
    Typescript
    SWEA
    싸피 7기
    SSAFY
    백준 1065
    노마드코더 Next
    styled component
    boj 1065
    노마드코더
    React
    싸피7기
    nextjs 시작하기
    NextJS
    백준
    노마드코더 nextjs
    김영한 스프링
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
9_yoon
[Web] JWT토큰(Access token, Refresh Token) 전달 방식, 저장 위치
상단으로

티스토리툴바