프로젝트 경험을 통해 다시보는 인증과 인가 — 로컬스토리지에서 HttpOnly 쿠키, 그리고 리프레시 토큰까지

시작: 모든 서비스는 인증과 인가로부터 시작된다

어떤 웹 서비스든 사용자의 로그인과 권한 관리는 핵심 기능이다. 사용자 정보를 안전하게 식별하고, 허용된 범위 내에서만 동작하게 만들기 위해선 반드시 인증(Authentication)과 인가(Authorization)의 개념을 명확히 이해하고, 설계에 반영해야 한다.

  • 인증: 사용자가 누구인지 확인하는 절차
  • 인가: 인증된 사용자가 어떤 자원에 접근할 수 있는지 결정하는 절차

서비스의 보안성, 확장성, 사용자 경험은 이 두 개념 위에서 결정된다.


1단계: LocalStorage 기반 JWT 인증의 문제점 인지

초기 프로젝트에서는 JWT 기반 인증을 사용했고, 액세스 토큰을 LocalStorage에 저장하는 방식을 채택했다. 구현이 간단하고 프론트엔드에서 직접 토큰을 읽고 붙일 수 있어 빠르게 개발할 수 있었지만, 보안 측면에서 심각한 취약점이 존재했다.

LocalStorage 방식의 문제

  • 자바스크립트로 직접 접근 가능 → XSS(크로스사이트 스크립팅) 공격에 매우 취약
  • 브라우저가 자동으로 전송하지 않음 → 개발 편의성이 떨어지고 수동 관리 필요
  • 보안 이슈가 있다는 것을 알면서도, 구체적인 위험성을 체감하지 못한 채 사용

이후 실제 보안 분석을 하면서, XSS 공격 시 사용자의 토큰이 그대로 탈취될 수 있다는 점을 명확히 인지하게 되었다.


2단계: HttpOnly 쿠키 기반 저장 방식 도입

이러한 취약점을 보완하기 위해 선택한 방식이 HttpOnly 쿠키 기반 JWT 저장이다.

HttpOnly 쿠키의 이점

  • 자바스크립트에서 접근 불가능 → XSS 공격 방어
  • 도메인과 경로에 따라 자동 전송 → 개발이 간단
  • Secure, SameSite 등의 옵션 조합으로 보안성 강화 가능

설정 예시

ResponseCookie cookie = ResponseCookie.from("accessToken", token)
    .httpOnly(true)
    .secure(true)
    .sameSite("Strict")
    .path("/")
    .maxAge(Duration.ofMinutes(30))
    .build();

하지만 도입 후에도 문제는 남아 있었다.


3단계: 액세스 토큰 만료 후 재발급이 안 되는 문제 발생

HttpOnly 쿠키로 전환한 이후, Access Token 만료 시 자동 재발급이 작동하지 않는 문제가 발견됐다. 사용자가 일정 시간 동안 아무 작업 없이 있다가 다시 요청을 보내면 401 Unauthorized가 반환되었고, 클라이언트는 강제로 로그아웃 상태가 되었다.

이 문제를 해결하기 위해 본격적으로 Refresh Token 전략을 공부하게 되었다.


4단계: Refresh Token과 Redis의 조합 학습

JWT 기반 인증 구조에서 Access Token은 짧게 유지하고, Refresh Token을 통해 재발급을 하는 것이 일반적인 보안 전략이다. 그러나 그동안 Refresh Token을 제대로 관리하지 않아, 아래와 같은 문제가 있었다:

  • Refresh Token을 어디에도 저장하지 않음 → 유효성 검증 불가
  • Rotation이나 블랙리스트 같은 고급 전략 미적용 → 탈취 시 대응 불가

Redis 저장 방식 도입

가장 실용적인 접근으로, Refresh Token을 Redis에 저장하고 TTL을 설정하는 방식을 채택했다.

redisTemplate.opsForValue().set(userId, refreshToken, Duration.ofDays(7));

이 방식은 다음과 같은 장점이 있다:

  • Redis는 빠른 응답과 TTL 설정이 가능
  • 유효성 검증, 강제 만료, 탈취 대응이 용이
  • 서버 재시작 후에도 토큰 유지 가능

5단계: 인증/인가 시스템 전체 설계 복기

이 과정을 거치며, 인증과 인가에 대한 전반적인 구조를 다시 정리하게 되었다.

인증

  • 로그인 시 ID/PW 검증 → Access, Refresh Token 발급
  • Access Token은 클라이언트 쿠키에 저장 (HttpOnly + Secure + SameSite)
  • 요청마다 서버는 Access Token을 검증해 사용자를 인증

인가

  • 인증된 사용자의 역할 정보를 바탕으로 권한 검사
  • 컨트롤러 단에서 @PreAuthorize("hasRole('ADMIN')") 등으로 접근 제한

토큰 만료 처리

  • Access Token 만료 시 Refresh Token을 통해 새 Access Token 발급
  • Refresh Token은 Redis에서 유효성 검사 후, 필요시 Rotation 적용

결론: 인증/인가의 실무 적용은 보안성과 사용자 경험의 균형

처음에는 단순하게 시작했던 인증 로직이었지만, 실제 사용자 경험과 보안 문제를 고려하면서 점점 더 정교한 시스템으로 발전했다. 다음과 같은 교훈을 얻었다:

  • 보안은 구현이 아닌 운영의 문제다 — 구현 후에도 검증과 점검이 필요하다
  • HttpOnly 쿠키는 XSS 방어의 핵심이다
  • Refresh Token은 반드시 상태 기반 관리가 필요하다 (Redis, DB 등)
  • 인증과 인가는 별개의 흐름이며, 각기 다른 설계 전략이 필요하다

이제는 JWT 기반 인증 시스템을 설계할 때, 처음부터 무상태 구조, 토큰 저장 전략, 재발급 흐름, 보안 대응을 포함한 전체 설계도를 함께 고려하는 것이 기본이 되었다.