인증 엔터셉터 구현

김현준·2025년 3월 28일

인증 방식 요약

스팟에디터는 카카오 소셜 로그인을 기반으로 한 JWT 인증 시스템을 사용한다.
토큰은 모두 httpOnly 쿠키에 저장되며, 이 방식은 보안성과 사용자 경험(자동 로그인 유지)에 중점을 두었다.

로그인 동작 흐름

  1. 사용자가 "카카오 로그인" 버튼을 클릭하면,
  2. 백엔드로 리다이렉트되어 소셜 로그인 처리가 진행된다.
  3. 로그인에 성공하면 백엔드는 Access Token(AT)Refresh Token(RT)
    httpOnly 쿠키로 프론트엔드에 전달한다.
  4. 이후 인증이 필요한 API 요청은 별도 처리 없이, 쿠키가 자동으로 포함되어 서버에서 인증된다.

즉, 프론트는 토큰을 직접 다루지 않아도 되고, 프론트는 withCredentials: true만 설정하면 인증이 처리된다.


왜 토큰을 쿠키에 담았을까?

먼저 XSS와 CSRF에 대해 알아보자

XSS(Cross-Site Scripting)

공격자가 사이트에 악성 스크립트를 삽입해서 다른 사용자의 브라우저에서 실행되도록 만드는 공격

예시:

  1. 공격자가 게시판에 <script>alert('해킹');</script> 같은 코드를 써서 올림
  2. 다른 사용자가 이 글을 보면 브라우저에서 스크립트가 실행 됨
  3. 이걸 이용해서 쿠키 탈취, 계정 탈취 등 가능함

누구를 공격하나?

  • 사이트에 접속한 사용자(다른 유저)

CSRF (Cross-Site Request Forgery)

사용자가 로그인 상태인 걸 악용해서 사용자가 의도하지 않은 요청을 서버에 보내는 공격

예시:

  1. 내가 bank.com에 로그인한 상태
  2. 공격자가 만든 사이트(evil.com)에 접속하면
  3. 거기서 몰래 <img src="https://bank.com/transfer?to=attacker&amount=1000"> 요청이 실행됨
  4. 서버는 내가 보낸 요청으로 착각하고 돈을 이체

누구를 공격하나?

  • 사이트의 서버를 속여서, 로그인된 사용자의 권한으로 요청을 보냄

XSS 공격과 CSRF 공격의 차이

XSS 공격 (사이트 간 스크립팅) 공격은 사용자가 웹사이트를 신용하여 악성 스크립트가 실행된다면, CSRF 공격은 반대로 특정 웹사이트가 사용자의 브라우저를 신용하여 발생하는 공격이다. 간단히 말해서, XSS 공격은 악성 코드가 클라이언트에서 발생하는데 반에, CSRF 공격은 악성 코드가 서버에서 발생한다고 볼 수 있다.

XSS 공격을 막는 것은 웹 보안을 위한 최소한의 조치

아무리 다른 공격(CSRF 등)에 대한 방비를 열심히 해도 XSS가 뚫린다면 아무 소용이 없다.

Js 코드로 의도하지 않은 request를 날린다던가
localStorage, 변수 값 등 모든 것이 탈취 가능하기 때문이다.

XSS 공격 방지는 웹 보안의 뿌리라고 할 수 있다.

JWT를 어디에 저장할까?

크게 2가지가 있다.

1. localStorage에 저장

  • 장점: CSRF 공격에는 안전
    자동으로 request에 담기는 쿠키와는 다르게
    js 코드에 의해 헤더에 담기므로 XSS를 뚫지 않는 이상
    공격자가 정상적인 사용자인 척 request를 보내기가 어렵다.

  • 단점: XSS에 취약
    공격자가 localStorage에 접근하는 Js 코드 한 줄만 주입하면
    localStorage를 공격자가 내 집처럼 드나들 수 있다.

2. cookie에 저장

  • 장점: XSS 공격으로부터 localStorage에 비해 안전
    쿠키의 httpOnly 옵션을 사용하면 Js에서 쿠키에 접근 자체가 불가능하다.
    그래서 XSS 공격으로 쿠키 정보를 탈취할 수 없다.
    (httpOnly 옵션은 서버에서 설정할 수 있음)

  • 단점: CSRF 공격에 취약
    자동으로 http request에 담아서 보내기 때문에
    공격자가 request url만 안다면
    사용자가 관련 link를 클릭하도록 유도하여 request를 위조하기 쉽다.

그래서 cookie와 localStorage 중에 어떤 것을 선택할까?

httpOnly 쿠키를 선택했다.
CSRF 공격은 다루기 쉬운 반면 프론트엔드 크기가 크면 클수록
XSS 공격을 막기위한 작업은 많아지므로 쿠키 사용이 나아보였다.

쿠키는 별도로 헤더에 담지 않아도 요청을 보낼때 자동으로 담아서 보내지기 때문에
매번 서버로의 요청에 담아야하는 토큰이라는 성격과 잘 맞고, 코드도 더 간결하게 작성할 수 있게 되는 것으로 보인다.

트러블슈팅 – 불필요한 인증 요청 문제

문제 상황

비로그인 사용자가 처음으로 스팟에디터에 진입했을 때,
홈 화면에서 일부 컴포넌트가 사용하는 axios 요청이 실행되며,
그 요청에 붙은 인터셉터가 /api/auth/refresh를 호출하게 된다.

하지만 이 사용자는 로그인하지 않았기 때문에 쿠키가 없어 refresh는 무의미한 시도가 되고,
결과적으로 불필요한 인증 관련 요청이 1회 발생하는 셈이다.

원인 분석

  • httpOnly 쿠키는 JS에서 접근 불가 → 토큰이 존재하는지 여부를 클라이언트에서 판단할 수 없음
  • 따라서 인증 여부와 상관없이 axios 요청이 발생하면 서버에 무조건 인증 관련 쿠키가 전달된다
  • 이 요청이 실패하면 인터셉터가 refresh 시도를 하게 되고, 그 자체가 불필요한 인증 요청이 된다

해결 방향 및 적용 방법

1. 인증 여부를 상태로 관리하도록 개선

// AuthClient 내부
private isAuthenticated: boolean | null = null;

async fetchAuthStatus() {
  try {
    await this.instance.get('/api/users');
    this.isAuthenticated = true;
  } catch {
    this.isAuthenticated = false;
  }
}
  • 앱 초기 진입 시 /api/users 요청을 통해 로그인 여부를 한 번 판단isAuthenticated에 저장
  • 이후엔 인터셉터에서 이 값을 보고 refresh 요청을 할지 말지 판단할 수 있음

2. 인터셉터 로직 수정

if (this.isAuthenticated === false) {
  // 로그인 안된 사용자 → refresh 시도 X
  return Promise.reject(error);
}

→ 이렇게 하면 비로그인 사용자에게 불필요한 refresh 요청이 발생하지 않는다.

최종 흐름 정리

  1. 앱 최초 진입 시 /로 인증 상태 확인
  2. 로그인한 사용자는 → 인증 유지
  3. 로그인하지 않은 사용자는 → 상태 관리로 refresh 시도 차단
  4. 홈 등 인증이 필요 없는 페이지에서는 인증 요청 자체를 막음 (enabled: false 등으로 조건부 쿼리 사용)

📎 참고: 왜 인증 요청 1번은 자연스러울 수도 있는가?

사용자가 로그인한 상태에서 탭을 닫고,
나중에 다시 직접 주소를 입력해서 접속하는 시나리오를 생각해보자.
이때 쿠키는 브라우저에 남아있기 때문에,
/api/users 같은 요청을 통해 자동으로 로그인 상태를 복구할 수 있다.
반대로 쿠키가 없으면 401 → 로그인 페이지로 안내하면 된다.

→ 따라서 이 인증 요청 1번은 보안성과 UX를 모두 챙기는 자연스러운 흐름이라고도 볼 수 있다.


마무리

스팟에디터는 보안과 사용성을 고려해 httpOnly 쿠키 기반 JWT 인증을 채택했고,
초기 인증 요청 1회는 필연적으로 발생하지만, 이를 통해 로그인 상태를 안정적으로 판단할 수 있다.

불필요한 인증 요청은 상태 기반 인증 흐름인터셉터 제어, axios 인스턴스 분리 등을 통해 충분히 관리할 수 있다.

profile
기록하자

0개의 댓글