1. 개요
DoEatFit (v2.0) 프로젝트의 백엔드 아키텍처 중 가장 치열하게 고민하고 완성도 높게 구축한 인증 및 인가 시스템의 핵심 기술적 고찰을 공유합니다. 서비스 볼륨이 확장되고 모바일 PWA 생태계 전반을 아우르게 되면서, 유저의 세션 및 토큰을 해킹 탈취 시나리오로부터 철벽 방어하는 동시에 서버 저장소 부하를 극적으로 차단하는 무상태(Stateless) JWT 기반 인증이 프로젝트 성공의 중대한 선결 조건으로 떠올랐습니다. Spring Security 6.x 필터 체인 설계와 Redis의 초고속 트랜잭션을 융합하여, 중복 로그인 및 모바일 불안정 수신 환경에서 발생하는 복잡한 레이스 컨디션(Race Condition)을 안전하게 잠재우고 토큰 재사용 공격(Replay Attack)을 원천 차단한 엔지니어링 기록을 정갈하게 정리해 드립니다.
2. 핵심 내용
2-1. 🚨 무상태 인증의 고민과 시행착오
- 1차 실패: RDB 저장 구조와 토큰 비활성화의 어려움:
- 초기에는 Access Token(AT)과 Refresh Token(RT)을 단순 매핑 발행한 뒤, 긴 만료 시간을 가진 RT를 관계형 데이터베이스(MySQL)에 영속화했습니다.
- 이로 인해 매 API 인입 요청마다 무거운 RDB에 접근하여 커넥션 풀을 과도하게 좀먹는 성능 병목에 직면했습니다. 더욱이 실시간 토큰 유출 시 이를 즉각 무효화(Revocation)할 비상 차단 수단이 부재했습니다.
- 2차 실패: Redis RTR의 도입과 레이스 컨디션의 급습:
- 병목 극복을 위해 토큰 스토어를 Redis 인메모리로 격상하고 보안 강화를 위해 토큰 재발급 요청 시 RT까지 강제 교체 발행하는 RTR(Refresh Token Rotation) 기법을 도입했습니다.
- 그러나 불안정한 모바일 PWA 특유의 망 연결 환경에서 클라이언트가 미시초 단위로 중복 갱신 API(
/api/auth/reissue-token)를 연이어 호출하는 중복 요청 사고가 발생했습니다. - 먼저 진입한 1번 요청이 RT를 검증해 파쇄하고 새 RT를 발급한 직후, 찰나의 순간 도달한 2번 요청은 이미 파괴된 이전 RT를 들고 왔기에, 보안 엔진은 이를 끔찍한 **“토큰 재사용 공격(Replay Attack)“**으로 간주하여 정상 사용자의 전체 세션을 강제 로그아웃시키는 정합성 결함을 초래했습니다.
2-2. 💡 원자적 RTR 설계와 유예기간 블랙리스트 구현
- Redis 원자적 연산(
GETDEL) 및 트랜잭션 확보:- 조회와 동시에 데이터를 강제 소멸시키는 레디스 고유의
GETDEL특성을 사용하고SessionCallback멀티 키 트랜잭션을 매핑하여, 검증과 삭제 단계 사이에 생기는 해킹의 틈새 레이스 컨디션을 원자적으로 원천 격리 제어했습니다.
- 조회와 동시에 데이터를 강제 소멸시키는 레디스 고유의
- 폐기 토큰 유예기간 블랙리스트(Revocation Grace Period) 수립:
- 찰나의 전송 렉으로 인입된 뒤늦은 정상 요청을 구출하기 위해, 소멸된 RT 식별자를 10초 내외의 매우 짧은 유예 시간 동안 **“임시 블랙리스트 스토어(
RVT:<token>)“**에 이메일 정보와 매핑해 수용해 줍니다. - RVT 시간 영역 내에 도달한 후속 갱신 요청은 정상적인 갱신 패킷 전달 과정으로 부드럽게 복구해 줍니다. 그러나 이를 이탈하거나 반복된 이상 거동 감지 시에는 명백한 해킹 침투로 간주하여, 해당
email명의의 모든 활성 RT군(RT:<token>)을 레디스에서 영구 전소(deleteAllTokensForUser)시키고 세션을 안전하게 폐쇄하여 보안을 철통 사수합니다.
- 찰나의 전송 렉으로 인입된 뒤늦은 정상 요청을 구출하기 위해, 소멸된 RT 식별자를 10초 내외의 매우 짧은 유예 시간 동안 **“임시 블랙리스트 스토어(
sequenceDiagram participant Client as PWA 모바일 클라이언트 participant Filter as JWT 보안 필터 (JwtFilter) participant AuthService as 인증 비즈니스 서비스 participant Redis as Redis 인메모리 스토어 participant Cookie as 쿠키 가드 (CookieUtil) Client->>Filter: API 요청 (AccessToken 만료 / RefreshToken 전달) Filter->>AuthService: reissueToken(AccessToken, Old RT) AuthService->>Redis: GET RVT:<Old RT> (이미 소멸된 폐기 토큰인가?) alt [보안 경고] 폐기 토큰 블랙리스트 스캔 완료 (재사용 공격!) Redis-->>AuthService: 피해자 이메일 검출 AuthService->>Redis: DEL RT:<모든RT> & user-tokens 강제 파쇄 (피해 계정 격리) AuthService-->>Client: 401 Unauthorized (세션 강제 폭파, 즉시 로그아웃) else [정상 통과] 블랙리스트 이력 없음 AuthService->>Redis: GETDEL RT:<Old RT> (원자적 조회 및 즉각 영구 소멸) alt 정상 존재 확인 AuthService->>Redis: SET RVT:<Old RT> (10초 유예기간 블랙리스트 안착) AuthService->>Redis: 신규 RT 레디스 트랜잭션 등록 (SET RT:<New RT>) AuthService->>Cookie: addRefreshTokenCookie(New RT, HttpOnly / Strict) AuthService-->>Client: 200 OK (새로운 AccessToken 응답 완료) else 비존재 토큰 (만료/변조) AuthService-->>Client: 401 Unauthorized (로그인 만료) end end
- Spring Security 6.x 정밀 튜닝 구성 (
UserSecurityConfig.java):- 무상태 인증 체계의 효율성을 극대화하기 위해 CSRF 및 기본 세션 쿠키 정책을
STATELESS로 격리하고 필터 예외 처리를 EntryPoint로 일원화 전파했습니다.
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(UrlPaths.NO_AUTH_API_PATHS).permitAll() .requestMatchers(UrlPaths.ADMIN_AUTH_REQUIRED_API_PATHS).hasAuthority(UserRoleEnum.ADMIN.getValue()) .anyRequest().authenticated() ) .addFilterBefore(new JwtFilter(tokenUtil), UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exception -> exception .authenticationEntryPoint(customAuthenticationEntryPoint) .accessDeniedHandler(customAccessDeniedHandler) ); return http.build(); } - 무상태 인증 체계의 효율성을 극대화하기 위해 CSRF 및 기본 세션 쿠키 정책을
2-3. ✅ 결과 검증 및 모던 보안 라이브러리 검토
- 결과 검증:
- 다중 브라우저 탭 및 PWA 디바이스를 기동해, 만료된 RT 토큰 셋으로 갱신 API를 연속 3회 동시 다발 호출하는 과부하 레이스 컨디션을 강제 연출했습니다.
- 시스템 로그 분석 결과, 1차 요청으로 정상 갱신된 AT/RT 세트가 200 OK로 전달되는 와중에 2차, 3차 중복 요청은 10초 유예기간 RVT 버퍼 덕택에 정상 수렴 처리되어 불필요한 전체 로그아웃 참변이 완벽히 해결되었음을 입증했습니다.
- 반면, 완전히 폐기된 과거 토큰으로 불시 접근할 때는 탈취 침입으로 판단해 Redis 전역 토큰을 3ms 만에 불태워 버리고 세션을 완전 파쇄시켰습니다.
- 추천 오픈소스 라이브러리 검토:
- Bucket4j
- 평가: 무차별 비밀번호 대입 공격이나 갱신 엔드포인트에 대한 브루트 포스(Brute Force) 어택을 미연에 방지하기 위해 Java 전용 경량 토큰 버킷 속도 제한기인 Bucket4j 장착을 추천합니다. IP나 세션 기준으로 초당 임계치 이상 통신 유입 시 RDB 도달 전에 즉시 입구 컷하여 인프라를 철벽 수호합니다.
- Nimbus JOSE + JWT
- 평가: 대규모 MSA 확장이나 OAuth2 표준 SSO 환경으로의 점진적 발돋움을 염두에 두신다면 비대칭 키(ECDSA, RSA) 서명과 JWK(JSON Web Key) 서명 로테이션 명세 제어력이 대단히 세련된 Nimbus 라이브러리 도입을 제안해 드립니다.
- Bucket4j