Welcome! Everything is fine.

[STUDY] Spring Security + JWT 로그인/로그아웃 흐름 본문

카테고리 없음

[STUDY] Spring Security + JWT 로그인/로그아웃 흐름

개발곰발 2025. 5. 29.
728x90

이번 주 스터디 주제는 인증/인가다!

최종 프로젝트에서 다른 팀원이 구현한 코드를 분석해보며 인증/인가에 대한 공부도 함께 진행해보려한다.

Spring Security + JWT를 사용하는 방식은 이전에 강의를 듣고 포스팅 한 적이 있지만,

프로젝트를 진행하면서 인증/인가 부분은 맡은 적이 없어서 기본에 충실한 내용들로 구성해 정리해보려 한다.

 

👤인증/인가가 필요했던 이유

우선 인증(Authentication)과 인가(Authorization)에 대해 짚고 가자.

  • 인증(Authentication)  : 사용자가 서비스에 가입되어 있는지 확인하는 과정(= 로그인)
  • 인가(Authorization)  : 인증된 사용자에 대해 권한을 확인하는 과정

인증/인가가 없다면 식당 예약, 이벤트 참여, 결제와 같은 주요 기능에 누구나 접근할 수 있어 보안상 문제가 발생할 수 있다.

또한 관리자만 접근 가능한 기능에 일반 유저가 접근할 수도 있다.

🧐왜 Spring Security + JWT 인가?

Spring Security

  • Spring Security : 스프링 기반 애플리케이션에서 가장 널리 쓰이는 보안 프레임워크.

공식 홈페이지에는 다음과 같이 설명되어 있다.

출처: https://spring.io/projects/spring-security

 

위 내용을 요약하자면 다음과 같다.

  • Spring 기반 애플리케이션의 사실상 표준 보안 프레임워크
  • 인증(Authentication)과 인가(Authorization) 모두 지원
  • 세션 고정, 클릭재킹, CSRF 등 보안 위협에 대한 내장 보호 기능
  • 필터 기반 구조로 커스터마이징이 용이
  • Spring Web MVC와도 쉽게 통합 가능
더보기
  • 세션 고정 : 공격자가 세션 ID를 미리 정해놓고, 그 세션 ID로 피해자가 로그인하게 만든 뒤, 같은 세션 ID를 이용해 피해자처럼 접근하는 공격. → 로그인 시 세션 ID를 자동으로 바꿔줌으로써 방어함.
  • 클락재킹 : 사용자가 알고 누른 게 아닌데, 누르게 만드는 공격. 공격자는 자신의 웹페이지 위에 투명한 iframe을 올려 유저가 어떤 버튼을 누르게 유도함.  → 기본적으로 X-Frame-Options 헤더를 통해 iframe 삽입을 차단함.
  • CSRF(Cross Site Request Forgery) : 사용자가 자신의 의지와 상관없이 공격자가 의도한 행위를 특정 웹사이트에 요청하도록 하는 것. → CSRF Token을 발급하고, 요청마다 서버에서 토큰 일치 여부 검증.

이렇게 Spring Security는 필터 기반 구조라 JWT 기반 인증 플로우를 쉽게 구현할 수 있었고, 인가 처리도 URL/메서드 단위로 유연하게 제어할 수 있어 선택했다.

 

실제로 최종 프로젝트에서 인가 처리는 @Secured 어노테이션이 메서드 단위로 적용되어 있다.

JWT(Json Web Token)

  • JWT(Json Web Token) : 토큰 자체에 정보가 포함되어 있는 클레임 기반 토큰으로, 통신 정보를 JSON 형식을 사용하여 안전하게 전송하기 위해 사용됨.

JWT의 구조

JWT의 구조는 다음과 같이 헤더(Header), 페이로드(Payload), 서명(Signature)으로 나뉜다.

출처: https://jwt.io/

  • 헤더(Header) : 토큰 타입, 서명 알고리즘
  • 페이로드(Payload) : 사용자 정보(Claims), 만료 시간 등
  • 서명(Signature) : 토큰 위조 방지용 서명

JWT의 특징

  • 서버가 상태를 저장하지 않음(Stateless) 서버가 이중화된 환경에서도 사용자의 로그인 정보를 일관성 있게 관리 가능
  • 세션 기반 인증에 비해서 사용자 정보를 조회하기 위한 추가적인 작업이 필요하지 않음
  • 디코딩이 쉬워 민감한 정보를 담는 것에 유의해야 함
  • JWT 탈취 시 위험   리프레시 토큰 도입 여부, 만료 시간 관리, 블랙리스트 등 보완 필요

최종 프로젝트에서는 Stateless 구조의 특성과, 수평 확장에 유리한 구조, 웹/모바일 등 다양한 클라이언트 환경과의 연동 편의성을 고려해 Spring Security와 연계한 JWT 기반 인증 방식을 선택했다.

세션 방식과 비교

항목 세션 기반 인증 JWT 기반 인증
상태 관리 Stateful(서버가 세션 저장) Stateless(서버가 상태 저장X)
인증 정보 저장 위치 서버 세션(JSESSIONID) 클라이언트가 토큰 저장
서버 확장성 세션 공유 필요(Sticky Session, Redis 등) 수평 확장에 유리
요청 인증 방식 쿠키에 JSESSIONID 자동 포함 Authorization 헤더에 Bearer 토큰 명시
로그아웃 처리 세션 무효화 블랙리스트 처리, Refresh Token 삭제
클라이언트 호환성 브라우저 중심 웹/모바일 등 다양한 클라이언트에 적합

 

💡로그인/로그아웃 흐름

로그인 전체 흐름

1. 로그인 요청 수신

클라이언트가 이메일과 비밀번호를 담아 로그인 요청을 보내고, 해당 요청은 아래 컨트롤러에서 처리된다.

  • Access Token : 본문 응답으로 전달
  • Refresh Token : CookieUtil을 통해 응답 쿠키에 저장

AuthController.java

2. 사용자 조회 및 탈퇴 여부 확인, 비밀번호 검증

  • 먼저 이메일로 사용자를 조회하고, 이미 탈퇴 처리된 사용자라면 예외를 발생시킨다. 즉, deletedAt 필드가 null이 아니라면 이미 탈퇴한 사용자로 간주한다.
  • 요청에서 받은 비밀번호와 DB에 저장된 비밀번호를 비교해 검증한다. 비밀번호 검증은 Spring Security에서 제공하는BCryptPasswordEncoder를 사용한다.

AuthService.java

 

3. Access Token & Refresh Token 생성

  • 인증이 완료된 유저 정보를 기반으로 JWT 토큰을 발급한다.
  • Access Token은 사용자 인증에 사용되고, Refresh Token은 재발급과 로그아웃 처리에 사용된다.

AuthService.java

 

🔹createAccessToken(user)

    • sub, userId, role, nickname, email, iat, exp 등을 claim에 포함한 JWT를 생성
    • JwtUtil.createToken() 메서드를 내부적으로 사용

TokenService.java
JwtUtil.java

🔹createRefreshToken(user)

    • Refresh Token도 JWT 형식으로 생성되며, userId를 값으로 Redis에 저장
    • 클라이언트엔 쿠키로 전달

TokenService.java

Refresh Token을 사용한 이유

Access Token만 사용한다면?

1. 사용자 경험 저하 가능성

Access Token은 Stateless 인증 수단으로, 서버가 별도로 저장하지 않아 관리가 어렵다. 따라서 토큰이 탈취될 경우의 피해를 줄이기 위해, 일반적으로 유효기간을 짧게 설정한다.

 

하지만 이 경우,

 

  • 유효기간이 짧기 때문에 사용자는 빈번하게 재로그인을 요구받게 된다.
  • 로그인 상태 유지가 어렵고, 사용자 경험이 저하된다.

 

2. 탈취 대응 어려움

Access Token은 발급 이후 서버가 따로 보관하지 않기 때문에 그 토큰이 누가 들고 있든, 만료 전까지는 무조건 유효하다.

 

즉, 토큰이 외부에 노출되거나 탈취되었을 경우 다음과 같은 문제점이 있다.

  • 서버는 이를 감지하거나 차단할 방법이 없다.
  • 유저 본인이 탈취를 인지하지 않는 이상 방어할 수 없다.
  • 특정 시점까지 공격자는 해당 사용자의 권한으로 모든 요청을 수행할 수 있다.

 

Refresh Token이 필요한 이유

1. 인증 상태를 유지하면서 UX를 보장

  • Access Token이 만료되더라도 Refresh Token으로 새로운 토큰을 재발급할 수 있다.

2. 탈취 대응 및 서버 측 제어 가능

  • Refresh Token은 서버에 저장되어 관리되기 때문에 로그아웃 시 삭제하거나 TTL 만료 설정이 가능하다.
  • 탈취 의심 시 즉시 무효화할 수 있다.
  • Refresh Token 자체는 HttpOnly + Secure 쿠키로 보호되며, 클라이언트에서 직접 접근할 수 없다.

실제 코드에서도 Refresh Token은 HttpOnly + Secure 옵션을 명시했다. 이렇게 설정함으로써 자바스크립트 접근 차단과  HTTPS 연결 제한을 동시에 적용하고 있다.

CookieUtil.java

로그아웃 전체 흐름

1. 로그아웃 요청 수신

클라이언트는 로그아웃 시 Authorization 헤더에 Access Token을 담고, Refresh Token 쿠키도 함께 전송한다.

해당 요청은 아래 컨트롤러에서 처리된다. 로그아웃이 완료되면 클라이언트 측 쿠키를 삭제한다.

AuthController.java

2. Refresh Token 제거 및 블랙리스트 등록

클라이언트가 보낸 Refresh Token을 Redis에서 제거하고,

Access Token은 블랙리스트에 등록해 더 이상 사용하지 못하도록 처리한다.

AuthService.java

Refresh Token은 Redis에 refreshToken:{token} 형태로 저장돼 있다.

로그아웃 시 이 값을 삭제함으로써 이후 해당 토큰을 사용한 Access Token 재발급 요청을 차단할 수 있다.

TokenService.java

Access Token은 서버에서 따로 관리하지 않기 때문에 한 번 발급되면 만료 전까지는 항상 유효하다.

이로 인해 사용자가 로그아웃하거나 탈퇴해도 누구든 기존의 Access Token을 계속 사용할 수 있다는 보안 취약점이 존재한다.

따라서 우리는 JWT 기반 인증 구조의 현실적인 보완책으로 블랙리스트 전략을 사용하고 있다.

 

🖤 블랙리스트(Blacklist)란?
블랙리스트란 더 이상 유효하지 않다고 판단되는 JWT Access Token을 별도의 저장소에 보관해두고, 이후 해당 토큰이 사용된 요청을 서버에서 차단하는 전략이다.

 

다음과 같이 토큰을 Redis에 블랙리스트로 등록하고, 남은 유효 시간만큼 TTL을 설정해 만료 전까지 서버에서 차단할 수 있도록 처리한다.

TokenService.java

 

  • Key: blacklistToken:{JWT}
  • Value: 이유 + 사용자 정보 (ex. logout:userId=123)
  • TTL: 토큰 만료 시점까지만 유지

Access Token이 블랙리스트에 등록되면, JwtAuthenticationFilter 내부에서 아래와 같은 순서로 토큰 유효성을 검사한다.

 

  1. Authorization 헤더에서 JWT 추출
  2. Redis에 해당 토큰이 블랙리스트로 등록되어 있는지 조회
  3. 등록된 토큰일 경우 → 인증 중단 + 401 응답 반환

따라서 불필요한 리소스 낭비를 막고, 빠르고 안전하게 요청을 거절할 수 있다.