React|성능 최적화와 보안

Access Token과 Refresh Token: 보안과 UX 사이의 안전한 인증 전략

14
Access Token과 Refresh Token: 보안과 UX 사이의 안전한 인증 전략

크리스는 로그인 기능을 구현하면서 가장 쉬운 길을 택했다.

서버에서 받은 토큰을 localStorage에 저장하고, API 요청을 보낼 때마다 꺼내서 헤더에 실어 보냈다.

// ❌ 크리스의 위험한 코드
localStorage.setItem('token', 'secret-token-123');
// 누군가 콘솔창에 localStorage.getItem('token')만 쳐도 털린다.

어느 날 보안 감사에서 "XSS(크로스 사이트 스크립팅) 공격에 취약함"이라는 경고를 받았다. 해커가 자바스크립트 코드를 심어서 사용자의 토큰을 탈취할 수 있다는 것이다.

그렇다면 토큰을 어디에 숨겨야 할까? 그리고 왜 토큰을 두 개나 써야 할까?

오늘은 프론트엔드 개발자가 반드시 알아야 할 보안과 사용자 경험(UX) 사이의 줄타기, 인증 토큰 전략을 파헤쳐 본다.

1. 왜 토큰을 두 개로 나눌까?

인증 시스템의 딜레마는 다음과 같다.

  • 보안: 토큰 유효기간을 짧게 설정해야 한다. (탈취당해도 피해 최소화)
  • UX: 사용자가 자주 로그인하게 만들면 안 된다. (귀찮음)
  • 이 두 마리 토끼를 잡기 위해 역할을 분담했다.

    Access Token (출입증)

  • 역할: 실제 API 데이터를 요청할 때 사용하는 열쇠.
  • 수명: 아주 짧다 (30분 ~ 1시간).
  • 특징: 자주 오가므로 탈취 위험이 있다. 그래서 수명을 짧게 해서, 털려도 금방 무용지물이 되게 만든다.
  • Refresh Token (재발급권)

  • 역할: Access Token이 만료되었을 때, 새것으로 교환하기 위한 증명서.
  • 수명: 아주 길다 (2주 ~ 한 달).
  • 특징: API 요청에는 쓰지 않고, 오직 로그인 연장에만 쓴다. 보안이 매우 강력한 곳에 보관해야 한다.
  • 2. 어디에 저장해야 안전할까? (Storage Wars)

    가장 논쟁이 뜨거운 주제다. 결론부터 말하면 "Refresh Token은 HttpOnly Cookie에, Access Token은 메모리(변수)에" 저장하는 것이 가장 안전하다.

    ❌ LocalStorage / SessionStorage

  • 장점: 구현이 세상에서 제일 쉽다.
  • 단점: 자바스크립트로 접근 가능하다(window.localStorage). 즉, XSS 공격에 뚫리면 해커가 토큰을 들고 튈 수 있다.
  • ✅ HttpOnly Cookie

  • 장점: 자바스크립트로 접근이 불가능하다(document.cookie로 안 보임). XSS 공격으로부터 안전하다.
  • 단점: CSRF(사이트 간 요청 위조) 공격에 취약할 수 있으나, 이는 SameSite 옵션으로 방어 가능하다.
  • 3. 이상적인 인증 흐름 (Best Practice)

    크리스가 채택해야 할 안전한 로그인 시나리오는 다음과 같다.

    1. 로그인 성공

    서버는 두 가지를 응답으로 보낸다.

  • Body: accessToken (JSON 데이터)
  • Header: Set-CookierefreshToken 설정 (HttpOnly, Secure, SameSite=Strict)
  • // 클라이언트는 accessToken만 받아서 변수에 저장한다.
    let accessToken = response.data.accessToken; 
    // refreshToken은 브라우저가 알아서 쿠키 깊숙한 곳에 저장한다.

    2. API 요청 (Access Token 사용)

    클라이언트는 메모리에 있는 accessTokenAuthorization 헤더에 담아 요청한다.

    headers: {
      Authorization: `Bearer ${accessToken}`
    }

    3. 토큰 만료 (401 Error)

    Access Token의 수명(30분)이 다했다. 서버는 401 Unauthorized 에러를 보낸다.

    이때 사용자를 로그아웃 시키면 안 된다. 사용자 모르게(Silent Refresh) 토큰을 갱신해야 한다.

    4. 토큰 재발급 (Refresh)

    클라이언트는 /refresh 엔드포인트로 요청을 보낸다.

    이때 브라우저가 자동으로 쿠키에 있는 Refresh Token을 실어 보낸다. (우리가 코드로 넣을 필요 없음)

    서버는 쿠키를 확인하고, 새로운 Access Token을 발급해준다.

    4. 보안 디테일 챙기기

    RTR (Refresh Token Rotation)

    Refresh Token조차 탈취당하면 어떡할까? 이를 막기 위해 RTR을 사용한다.

    Refresh Token을 한 번 쓰면 무조건 폐기하고 새로 발급받는 방식이다.

    만약 해커가 훔친 Refresh Token을 사용하면? 이미 사용된 토큰이므로 서버는 이를 감지하고 모든 토큰을 즉시 차단해버린다.

    새로고침 문제 해결

    Access Token을 메모리(변수)에 저장하면, 페이지를 새로고침 할 때마다 날아간다.

    그래서 앱이 초기화될 때(Mount), 가장 먼저 /refresh API를 찔러서 Access Token을 받아오는 과정이 필요하다.

    핵심 정리

  • Access Token: 짧게 산다. API 요청용이다. 메모리(변수)에 저장하는 것이 원칙이다.
  • Refresh Token: 길게 산다. 갱신용이다. HttpOnly Cookie에 저장하여 자바스크립트 접근을 막는다.
  • XSS 방어: 로컬 스토리지 사용을 지양한다.
  • UX: 사용자는 토큰이 만료되었는지도 모르게, 백그라운드에서 자동으로 갱신되어야 한다.
  • 개념은 완벽하다. 하지만 이것을 매번 API 요청할 때마다 if (tokenExpired) refresh() 코드를 짤 수는 없다.

    이 복잡한 갱신 로직을 단 한 곳에서 자동으로 처리해 주는 문지기, Interceptors를 구현할 차례다.

    "Axios Interceptor를 통한 토큰 갱신과 토큰 블랙리스트, 화이트리스트 처리" 편에서 계속된다.


    🔗 참고 링크

  • MDN - HttpOnly Cookie
  • SameSite Cookies Explained
  • OWASP - Session Management
  • 댓글 (0)

    0/1000
    댓글을 불러오는 중...
    Access Token과 Refresh Token: 보안과 UX 사이의 안전한 인증 전략 | VXD Blog