Skip to content

[FLYW-140] CSRF 설정 분리 (JSP Web 활성화 / API Origin 검증 적용) - #261

Merged
ochanhyeok merged 18 commits into
devfrom
FLYW-140-CSRF-설정-분리-(JSP-Web-활성화-%2F-API-Origin-검증-적용)
Feb 13, 2026

Hidden character warning

The head ref may contain hidden characters: "FLYW-140-CSRF-\uc124\uc815-\ubd84\ub9ac-(JSP-Web-\ud65c\uc131\ud654-%2F-API-Origin-\uac80\uc99d-\uc801\uc6a9)"
Merged

ochanhyeok merged 18 commits into
devfrom
FLYW-140-CSRF-설정-분리-(JSP-Web-활성화-%2F-API-Origin-검증-적용)

Conversation

@gaeunnlee

@gaeunnlee gaeunnlee commented Feb 11, 2026 •

Copy link
Copy Markdown
Member

📌 PR 설명

쿠키 기반 인증(JWT Cookie) 환경에서 상태 변경 요청(POST/PUT/PATCH/DELETE)에 대한 CSRF 방어를 강화하고, 추가로 Origin/Referer 검증 레이어를 도입하여 교차 출처 요청 위조를 이중 방어하도록 개선했습니다.

또한 CSRF 토큰을 안정적으로 전달하기 위해 토큰 부트스트랩 엔드포인트 및 프론트 유틸을 추가하고, 검증 필터에 대한 단위 테스트를 함께 작성했습니다.

상태 변경 요청에 대한 동작 과정

  • Origin/Referer 검증 → CSRF 검증 → JWT 인증 → 온보딩 접근 제어

팀 공용 csrfFetch 함수 추가

  • 로그인(인증)이 필요 없는 요청 → csrfFetch 사용
  • 로그인(인증)이 필요한 요청 → 기존 fetchWithRefresh 사용
    fetchWithRefresh 내부에는 이미 CSRF 검증 로직(XSRF 쿠키 확인 + X-XSRF-TOKEN 헤더 첨부) 이 포함되어 있으므로
    인증 요청에서는 csrfFetch를 따로 사용할 필요 없습니다.
  • 예시
    import { csrfFetch } from "/resources/common/js/csrfFetch.js";
    
    await csrfFetch("/api/sms/send?phoneNumber=01012345678", {
      method: "POST",
    });

✅ 완료한 기능 명세

  • CSRF 보호 활성화 (Web + API)
    • Web Security 체인에서 CSRF 보호 활성화
    • API Security 체인(/api/**)에도 CSRF 보호 활성화
    • CSRF 토큰 부트스트랩 엔드포인트 추가
      • GET /auth/csrf 호출 시 CSRF 토큰 쿠키 발급/갱신 가능
  • 프론트 CSRF 요청 유틸 추가 및 적용
    • 공통 유틸 csrfFetch.js 추가
      • 상태 변경 메서드(POST/PUT/PATCH/DELETE) 요청 시 자동 처리:
        • XSRF-TOKEN 쿠키 존재 여부 확인
        • 없을 경우 /auth/csrf 호출하여 토큰 부트스트랩
        • X-XSRF-TOKEN 헤더 자동 첨부 후 fetch 실행
    • 기존 인증 요청 유틸 authFetch.js가 csrfFetch를 사용하도록 변경
      • 인증/인가 요청에서도 CSRF 검증이 자동으로 적용되도록 통합
  • JSP/Form 기반 CSRF 적용 확대
    • 주요 JSP 페이지 및 폼에 <sec:csrfInput/> 적용
  • Origin/Referer 검증 필터 신규 도입
    • OriginRefererCheckFilter 신규 구현
      • CSRF 토큰 외 추가적인 방어 레이어 제공
      • 브라우저 기반 Cross-Origin 요청 위조 차단 강화
    • 상태 변경 메서드만 검증하도록 제한
      • POST / PUT / PATCH / DELETE만 검사
      • GET 요청 및 정적 리소스 영향 최소화
      • OPTIONS 요청은 CORS preflight 고려하여 검사 제외
    • 검증 우선순위 적용
      • Origin 헤더가 존재하면 Origin 우선 검증
      • Origin이 없을 경우 Referer 기반 fallback 검증
    • 허용되지 않은 출처 요청은 즉시 차단
      • 검증 실패 시 403 Forbidden 응답 처리
  • Security FilterChain 반영 (API/Web 분리 적용)
    • API Security 체인에서 필터 적용
      • /api/** 요청에서 CsrfFilter 이전 단계로 Origin/Referer 검증 필터 실행
      • API 요청의 출처 검증 → CSRF 검증 → JWT 인증 순으로 실행되도록 구성
    • Web Security 체인에서 필터 적용
      • Web 환경에서는 특정 경로만 검사
        • 검사 대상: /mypage, /reservations, /payment, /payments
        • 예외 경로: /oauth/**, /auth/**, /loginProc

📸 스크린샷

API Chain - Origin/Referer + CSRF 검증 흐름

API(1)

CSRF 토큰 발급 과정

csrfFetch 동작 과정

💭 고민과 해결과정

1) CSRF 보호 적용 범위 확장

쿠키 기반 인증(JWT Cookie) 구조에서는 브라우저가 자동으로 쿠키를 포함해 요청을 전송하기 때문에, 상태 변경 요청이 CSRF 공격에 취약해질 수 있었습니다.

이를 해결하기 위해 다음을 적용했습니다.

  • Web Security에 CSRF 활성화 및 CookieCsrfTokenRepository.withHttpOnlyFalse() 적용
  • API Security(/api/**)에도 동일하게 CSRF 활성화 적용
  • CSRF 토큰을 최초 발급받을 수 있도록 부트스트랩 엔드포인트 추가 (GET /auth/csrf)
  • 프론트에서 상태 변경 요청 시 XSRF-TOKEN 쿠키 확보 후 X-XSRF-TOKEN 헤더 자동 첨부하도록 csrfFetch.js 유틸 추가
  • 기존 authFetch.js가 csrfFetch를 사용하도록 반영
  • 주요 JSP/Form 페이지에 <sec:csrfInput/> 적용 (login, signup, home, reservations 등)

2) Origin/Referer 검증 필터 추가 (이중 방어)

CSRF 토큰 검증만으로는 토큰 탈취/오용 가능성을 완전히 배제하기 어렵기 때문에, 추가적으로 요청 출처를 확인하는 방식을 도입했습니다.

  • OriginRefererCheckFilter 신규 구현
  • 상태 변경 메서드(POST/PUT/PATCH/DELETE)만 검사, OPTIONS는 제외
  • Origin 헤더 우선 검증, 없으면 Referer fallback 검증
  • 허용되지 않은 출처 요청은 403 Forbidden 처리
  • API/Web 보안 체인에 CsrfFilter 이전 단계로 필터 적용

또한 Web 환경에서는 전체 경로를 무조건 검사할 경우 불필요한 영향이 생길 수 있어,
검사 범위를 제한했습니다.

  • Web 대상 경로만 검사: /mypage, /reservations, /payment, /payments
  • 예외 처리: /oauth/**, /auth/**, /loginProc

🔗 관련 이슈

Closes #100


Summary by CodeRabbit

  • 새로운 기능

    • 검색 엔드포인트가 POST→GET으로 변경되어 쿼리 매개변수로 검색 가능
    • /auth/csrf 읽기용 엔드포인트 추가
    • 로그인 시 안전한 returnUrl 리다이렉트 지원
    • csrfFetch 유틸 글로벌 도입 및 클라이언트 전면 적용
    • 리프레시 토큰 강제 로그아웃 및 토큰 일괄 폐기 기능 추가
  • 보안 개선

    • JSP 폼과 JS 호출에 CSRF 토큰 적용
    • Origin/Referer 검사 필터 추가(웹/API)
    • 리프레시 토큰 동기화 필터로 세션·인증 정리 강화
  • 테스트

    • CSRF 및 Origin/Referer 동작 검증 단위 테스트 추가

- refresh token 검증 실패/만료/재사용/레이스(이미 회전됨) 등 예외 케이스에서 단순 에러 반환 대신 forceLogout()로 보안 컨텍스트/세션/쿠키를 일괄 정리하도록 개선
- refreshToken 쿠키 path를 '/'로 통일하고, 과거 '/auth' 경로로 남아있는 레거시 쿠키도 함께 삭제 (deleteRefreshCookies로 두 경로 동시 정리)
- WEB(SecurityConfigWeb) 체인에 RefreshTokenSessionSyncFilter를 추가하여, 로그인 상태인데 refresh token 쿠키/DB 상태가 불일치할 경우 즉시 강제 로그아웃 후 /login 리다이렉트
- 필터 적용 범위에서 정적 리소스/퍼블릭 엔드포인트(/error 포함)는 제외하여 불필요한 로그아웃 방지

Motivation:
- 하이브리드(Session + JWT Cookie) 구조에서 refresh token 유실/폐기 상태로 인증 컨텍스트만 남는 경우 처리
- API 요청에서 JWT로 인증된 요청인지 명확히 구분하기 위해 `JWT_AUTHENTICATED_ATTR` 마커를 도입
- `authenticate()` 시점에
  - `req.setAttribute(JWT_AUTHENTICATED_ATTR, true)`로 **요청 단위** 마킹
  - `auth.setDetails(JWT_AUTHENTICATED_ATTR)`로 **SecurityContext 인증 객체**에도 마킹
- 기존 `isAlreadyAuthenticated()`(Anonymous 제외) 방식 대신,
  - “이미 JWT로 인증된 요청”만 스킵하도록 `isJwtAuthenticatedRequest()`로 로직 변경
- /login 요청에서 returnUrl 파라미터를 Model에 전달하도록 수정
- returnUrl sanitize 처리로 외부 URL(Open Redirect) 위험 차단
- login.jsp에 returnUrl hidden input 추가하여 로그인 요청 시 전달
- 로그인 성공 시 returnUrl 파라미터가 존재하면 우선적으로 redirect 처리
- returnUrl sanitize 검증 추가하여 악성 redirect 방지
- 기존 request attribute 기반 redirect 로직과 병행 처리
- WebLoginRedirectEntryPoint 추가하여 인증 실패 시 /login?returnUrl=... redirect 처리
- SecurityConfigWeb exceptionHandling에 entrypoint 등록
- /login 요청에 대한 무한 redirect 방지 로직 포함
- JwtWebAuthFilter에서 인증 실패 시 entrypoint 대신 login redirect 처리
- RefreshTokenSessionSyncFilter 강제 로그아웃 시 returnUrl 포함 redirect 적용
- GET 요청에 한해 returnUrl 생성, 외부 redirect 방지 검증 로직 포함
- /login 경로는 JwtWebAuthFilter에서 제외하여 무한 루프 방지
@coderabbitai

coderabbitai Bot commented Feb 11, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

CSRF 쿠키 기반 보호와 Origin/Referer 검증 필터(API·Web), 리프레시 토큰 기반 세션 동기화 필터가 도입되었고, 로그인 리다이렉트(returnUrl) 검증 및 강제 로그아웃(forceLogout) 흐름이 추가되었습니다. 프론트엔드에 csrfFetch 유틸이 추가되고 JSP 폼에 CSRF 입력이 삽입되었습니다.

Changes

Cohort / File(s) Summary
인증 엔드포인트·토큰 서비스
src/main/java/com/flyway/auth/controller/AuthController.java, src/main/java/com/flyway/auth/service/AuthTokenService.java, src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java
/auth/csrf GET 엔드포인트 추가; forceLogout(HttpServletRequest,HttpServletResponse) 인터페이스·구현 추가; REFRESH 쿠키 경로 기본값을 /로 변경하고 레거시 /auth 경로도 삭제 처리(삭제 대상 쿠키 동시 제거); 여러 에러 경로에서 forceLogout 호출 및 세션·SecurityContext 정리 로직 추가.
뷰·리다이렉트 처리
src/main/java/com/flyway/auth/controller/AuthViewController.java, src/main/java/com/flyway/security/handler/LoginSuccessHandler.java, src/main/java/com/flyway/security/handler/WebLoginRedirectEntryPoint.java
로그인 뷰와 성공 핸들러에 returnUrl 파라미터 수용 및 sanitizeReturnUrl 검증 추가; WebLoginRedirectEntryPoint 도입으로 비인증 요청에 안전한 /login 리다이렉트 생성.
보안 설정 및 필터
src/main/java/com/flyway/security/config/SecurityConfigApi.java, src/main/java/com/flyway/security/config/SecurityConfigWeb.java, src/main/java/com/flyway/security/filter/OriginRefererCheckFilter.java, src/main/java/com/flyway/security/filter/RefreshTokenSessionSyncFilter.java
API/Web 보안 설정에 OriginRefererCheckFilter 추가(필터 빈·체인 배치) 및 CSRF를 CookieCsrfTokenRepository로 활성화(특정 엔드포인트 예외 등록). RefreshTokenSessionSyncFilter 추가로 리프레시 토큰 유효성 검사·강제 로그아웃·리다이렉트 흐름 도입 및 필터 체인 재배치.
JWT 인증 필터 변경
src/main/java/com/flyway/security/jwt/JwtApiAuthFilter.java, src/main/java/com/flyway/security/jwt/JwtWebAuthFilter.java
요청 수준 JWT 인증 플래그(JWT_AUTHENTICATED_ATTR) 도입으로 재진입 검사 개선; JwtWebAuthFilter는 /login 우회 처리 및 인증 실패 시 로그인 리다이렉트(redirectToLogin) 로직 추가.
검색 API
src/main/java/com/flyway/search/controller/FlightApiController.java, src/main/java/com/flyway/search/dto/FlightSearchRequest.java
flights 검색 엔드포인트를 POST→GET으로 전환하고 요청 바인딩을 ModelAttribute/쿼리파라미터로 변경. LocalDate 필드에 @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) 적용.
프론트엔드 CSRF 유틸 및 네트워크 변경
src/main/webapp/resources/common/js/csrfFetch.js, src/main/webapp/resources/common/js/authFetch.js, 여러 resources/*/*.js, JSP include 파일들
새로운 csrfFetch 유틸 추가(부트스트랩, 쿠키/헤더 관리, mutating 요청에 CSRF 헤더 주입) 및 기존 fetch 호출들을 csrfFetch로 교체. authFetch가 csrfFetch 기반으로 재구성됨. JSP 헤드 include에서 csrfFetch 모듈 노출 추가.
JSP CSRF/태그 적용
여러 src/main/webapp/WEB-INF/views/* (login.jsp, signup.jsp, header.jsp, booking.jsp, agreement.jsp, 등)
JSP에 Spring Security 태그라이브러리(sec) 추가 및 폼에 <sec:csrfInput/> 삽입, 헤드 include로 csrfFetch 모듈 로드 추가.
구성·프로퍼티
src/main/java/com/flyway/security/config/SecurityOriginProperties.java, src/main/resources/config/application-prod.properties
허용 Origin 목록을 파싱해 제공하는 SecurityOriginProperties 추가 및 security.allowed-origins 프로퍼티 추가(예: https://flyway.kr,...).
리보케이션 서비스·테스트
src/main/java/com/flyway/auth/service/RefreshTokenRevocationService.java, src/test/java/...
RefreshTokenRevocationService 추가(리프레시 토큰 일괄 폐기, 트랜잭션 경계 설정) 및 관련 단위 테스트 추가/수정(Origin/Referer 필터, CSRF 보호, 재사용된 리프레시 토큰 검증 등).

Sequence Diagrams

sequenceDiagram
    participant Client
    participant OriginFilter as OriginReferer\n(API)
    participant CsrfFilter as CSRF\n(CookieRepo)
    participant Controller as AuthController
    Client->>OriginFilter: POST /api/... (Origin/Referer)
    activate OriginFilter
    OriginFilter->>OriginFilter: isStateChanging? / isExcluded?
    alt Included & Not excluded
        OriginFilter->>OriginFilter: validate Origin/Referer
        alt Allowed
            OriginFilter->>CsrfFilter: forward request
            activate CsrfFilter
            CsrfFilter->>CsrfFilter: validate CsrfToken (cookie/header)
            alt Valid
                CsrfFilter->>Controller: forward
                Controller-->>Client: 2xx / delegate
            else Invalid
                CsrfFilter-->>Client: 403 Forbidden
            end
            deactivate CsrfFilter
        else Blocked
            OriginFilter-->>Client: 403 Forbidden
        end
    else Excluded
        OriginFilter->>CsrfFilter: forward
    end
    deactivate OriginFilter
Loading
sequenceDiagram
    participant Browser
    participant JwtFilter as JWT\nAuthFilter
    participant RefreshSync as RefreshToken\nSessionSyncFilter
    participant RefreshRepo as RefreshToken\nRepository
    participant AuthService as AuthTokenService
    Browser->>JwtFilter: GET /protected (with JWT)
    activate JwtFilter
    JwtFilter->>JwtFilter: isJwtAuthenticatedRequest?
    JwtFilter->>RefreshSync: forward
    deactivate JwtFilter

    activate RefreshSync
    RefreshSync->>RefreshSync: isAuthenticated?
    alt Authenticated
        RefreshSync->>RefreshSync: readCookie(refreshToken)
        RefreshSync->>RefreshRepo: lookup(hashedToken)
        alt Token valid
            RefreshSync-->>Browser: proceed (200)
        else Token invalid/missing
            RefreshSync->>AuthService: forceLogout(request,response)
            activate AuthService
            AuthService->>AuthService: delete cookies, invalidate session, clear SecurityContext
            AuthService-->>Browser: redirect /login?returnUrl=...
            deactivate AuthService
        end
    else Not authenticated
        RefreshSync-->>Browser: proceed (skip)
    end
    deactivate RefreshSync
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

FEAT, ready-for-review

Suggested reviewers

  • cl-o-lc

Poem

🐰 쿠키 한 알, 토큰 한 조각 들고,
출처도 살피며 길을 비추었지요.
csrfFetch 춤추며 안전하게 달려,
리다이렉트는 깨끗이 정리하고,
토끼가 속삭여요: "이제 안심해요!"

🚥 Pre-merge checks | ✅ 3 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Invalid branch name format
Out of Scope Changes check ❓ Inconclusive 모든 변경사항이 CSRF 설정 분리 및 Origin/Referer 검증 범위 내에 있습니다. 다만 회원가입 테스트에 phoneNumber 필드 추가는 SMS 인증과 관련된 테스트 수정으로 간접적 관련이 있으나, 주요 CSRF 구현과는 독립적입니다. phoneNumber 필드 추가가 회원가입 테스트 유지 관리와 관련된 것인지, 아니면 별도의 기능 변경과 관련된 것인지 명확히 하시기 바랍니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 CSRF 설정 분리와 JSP Web 활성화, API Origin 검증 적용이라는 주요 변경사항을 명확하게 요약하고 있으며, 코드베이스의 주요 변경과 일치합니다.
Linked Issues check ✅ Passed 코드 변경사항이 #100 (FLYW-140)의 CSRF 설정 분리 요구사항을 충족합니다: CSRF 보호 활성화, CSRF 토큰 부트스트랩 엔드포인트, OriginRefererCheckFilter 구현, JSP CSRF 입력 추가, 필터 체인 순서 정의 등이 모두 구현되어 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FLYW-140-CSRF-설정-분리-(JSP-Web-활성화-%2F-API-Origin-검증-적용)
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch FLYW-140-CSRF-설정-분리-(JSP-Web-활성화-%2F-API-Origin-검증-적용)
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gaeunnlee gaeunnlee changed the title [FLYW-140] csrf 설정 분리 (jsp web 활성화 & api origin 검증 적용) [FLYW-140] CSRF 설정 분리 (JSP Web 활성화 / API Origin 검증 적용) Feb 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java (1)

78-136: ⚠️ Potential issue | 🔴 Critical

refresh() 트랜잭션 내에서 forceLogout() 호출 시 DB 변경이 롤백됩니다.

refresh()는 @Transactional로 선언되어 있어, forceLogout() → logout() → revokeById() / revokeAllByUserId() 등의 DB 변경이 수행된 후 BusinessException이 throw되면 트랜잭션 전체가 롤백됩니다.

특히 재사용 탐지 경로(Line 102-106)에서 revokeAllByUserId()는 보안상 중요한 조치인데, 롤백으로 인해 실제 DB에 반영되지 않습니다. 결과적으로 클라이언트 측 쿠키/세션은 정리되지만 서버 측 토큰은 유효한 상태로 남게 됩니다.

해결 방안:

  1. forceLogout 내 DB 작업을 REQUIRES_NEW 전파 수준의 별도 트랜잭션으로 분리
  2. 또는 refresh() 내에서 throw 전에 forceLogout을 호출하지 않고, 호출자(컨트롤러)에서 예외 catch 후 forceLogout 실행
🤖 Fix all issues with AI agents
In `@src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java`:
- Around line 15-19: AuthTokenServiceImpl 내 import 목록에 SecurityContextHolder가 중복
선언돼 있으니 중복된 import 한 줄을 삭제해 단일 선언만 남기세요 (참조 식별자: SecurityContextHolder, 클래스:
AuthTokenServiceImpl).

In `@src/main/java/com/flyway/security/filter/OriginRefererCheckFilter.java`:
- Around line 180-186: normalizeOrigin currently trims and strips a trailing
slash but doesn't normalize case, so origins like "HTTPS://FLYWAY.KR" won't
match the allowlist; update the normalizeOrigin method to trim, remove any
trailing slash, and convert the result to a consistent case (use
toLowerCase(Locale.ROOT)) before returning so scheme/host comparisons are
case-insensitive (keep the method name normalizeOrigin and update its return
value accordingly).
- Around line 26-29: The DEFAULT_ALLOWED_ORIGINS constant in
OriginRefererCheckFilter currently hardcodes "http://localhost:8080", which is
unsafe; change the filter to load allowed origins from external configuration
(e.g., application properties or an environment variable) instead of relying on
the hardcoded DEFAULT_ALLOWED_ORIGINS: refactor OriginRefererCheckFilter to read
a configured List<String> (inject via constructor or
`@Value/`@ConfigurationProperties) and use that for origin checks, keep a minimal
safe default if none provided but do not include localhost in production
defaults, and update any references to DEFAULT_ALLOWED_ORIGINS to use the
injected/loaded value.

In `@src/main/java/com/flyway/security/jwt/JwtWebAuthFilter.java`:
- Around line 52-56: The current check in JwtWebAuthFilter using
path.startsWith("/login") is too broad and skips routes like /loginProc; update
the condition in the code around resolvePath(request) so it only bypasses the
exact login page or subpaths under /login (e.g. change path.startsWith("/login")
to path.equals("/login") || path.startsWith("/login/")); adjust the branch where
filterChain.doFilter(request, response) is invoked so only true /login or
/login/* paths are skipped and other paths such as /loginProc still go through
JWT validation.

In `@src/main/webapp/resources/common/js/csrfFetch.js`:
- Around line 87-95: Race condition: non-module scripts may call
window.csrfFetch before the ES module assigns it; to fix, immediately set
window.csrfFetch at module initialization to a defensive wrapper that queues
calls (or returns a rejected Promise with a clear error) and then replace it
with the real exported csrfFetch once ready; specifically, add a top-level early
assignment for window.csrfFetch in the csrfFetch module that references
csrfFetch (the exported function), and inside the real csrfFetch continue using
isStateChanging, ensureCsrfCookie, toFetchUrl, and withCsrfHeader so queued
calls are executed with the correct behavior once the module finishes
initialization.
🧹 Nitpick comments (16)
src/main/java/com/flyway/security/jwt/JwtWebAuthFilter.java (2)

125-139: buildReturnUrl 검증 로직이 4곳에 중복됨

buildReturnUrl(이 파일), WebLoginRedirectEntryPoint.buildReturnUrl, LoginSuccessHandler.sanitizeReturnUrl, AuthViewController.sanitizeReturnUrl — 거의 동일한 open-redirect 방지 로직이 4곳에 복사되어 있습니다. 검증 규칙이 변경되면 모든 곳을 동시에 수정해야 하므로 유지보수 리스크가 있습니다.

공통 유틸리티 클래스(예: ReturnUrlSanitizer)로 추출하면 일관성과 유지보수성이 향상됩니다.


34-35: entryPoint 필드는 사용되지 않으므로 제거하세요

redirectToLogin() 메서드로 전환되면서 entryPoint.commence() 호출이 모두 제거되었습니다. 해당 필드는 더 이상 코드 내에서 참조되지 않으므로 불필요한 의존성입니다. 함께 불필요한 import도 제거할 수 있습니다.

src/main/java/com/flyway/security/handler/WebLoginRedirectEntryPoint.java (1)

27-30: 무한 루프 방지 로직 — /login 요청 시에도 redirect 발생

/login은 SecurityConfigWeb에서 public endpoint로 설정되어 있으므로 일반적으로 이 EntryPoint가 호출되지 않을 것입니다. 하지만 방어적 코드로 유지하는 것은 합리적입니다.

다만, 현재 로직은 /login 경로일 때 다시 /login으로 redirect하는데, 이미 /login에 있는 사용자가 인증 실패로 여기에 도달한 경우 불필요한 redirect가 발생합니다. response.sendRedirect 대신 response.sendError(HttpServletResponse.SC_UNAUTHORIZED) 또는 단순히 filterChain을 통과시키는 것도 고려해 볼 수 있습니다.

src/main/webapp/resources/common/js/csrfFetch.js (1)

53-66: ensureCsrfCookie에서 bootstrap fetch 실패를 무시함

Line 63의 빈 catch (e) {} 블록이 네트워크 에러나 서버 에러를 완전히 삼킵니다. CSRF 토큰 부트스트랩이 실패한 이유를 디버깅하기 어렵습니다.

제안: 최소한의 경고 로그 추가
-    } catch (e) {}
+    } catch (e) {
+        console.warn("[csrfFetch] CSRF bootstrap failed", e);
+    }
src/main/webapp/resources/search/js/filtering.js (1)

22-30: loadAirlines에 에러 처리가 없습니다.

csrfFetch 전환 자체는 적절하지만, 네트워크 오류나 비정상 응답 시 res.json()이 예외를 발생시킬 수 있고, 이 함수는 DOMContentLoaded의 await에서 호출되므로 unhandled rejection이 될 수 있습니다. 기존 코드의 문제이지만, 이번 변경과 함께 방어 로직을 추가하는 것을 고려해 보세요.

♻️ 에러 처리 추가 제안
 async function loadAirlines() {
-    const res = await csrfFetch(`${CONTEXT_PATH}/api/public/airlines`);
-    const data = await res.json();
-
-    AIRLINES = data.map(a => ({
-        code: a.airlineId,
-        name: a.airlineName
-    }));
+    try {
+        const res = await csrfFetch(`${CONTEXT_PATH}/api/public/airlines`);
+        if (!res.ok) return;
+        const data = await res.json();
+        AIRLINES = data.map(a => ({
+            code: a.airlineId,
+            name: a.airlineName
+        }));
+    } catch (e) {
+        console.error("항공사 목록 로딩 실패:", e);
+    }
 }
src/main/webapp/resources/mypage/js/render/bookings.js (1)

27-34: getAirlineMap이 detail.js와 중복됩니다.

bookings.js의 getAirlineMap (Lines 27-34)과 detail.js의 getAirlineMap (Lines 26-33)이 toAssetUrl 헬퍼와 함께 거의 동일한 구현입니다. 공통 모듈로 추출하면 유지보수가 용이해집니다.

src/main/webapp/resources/auth/signup.js (1)

50-58: 응답 Body 이중 소비 가능성이 있습니다.

Line 53의 res.json()이 실패하면 Body 스트림이 이미 소비된 상태이므로, Line 56의 res.text()도 실패할 수 있습니다. 기존 코드의 문제이지만, 이번 변경 시 함께 수정하는 것을 고려해 보세요.

♻️ 응답 텍스트를 먼저 읽고 JSON 파싱하는 방식 제안
-            let data = null;
-            try {
-                data = await res.json();
-                attemptIdHidden.value = data.data.attemptId;
-            } catch (e) {
-                const text = await res.text().catch(() => "");
-                data = text ? { message: text } : {};
-            }
+            let data = null;
+            const text = await res.text();
+            try {
+                data = JSON.parse(text);
+                attemptIdHidden.value = data.data.attemptId;
+            } catch (e) {
+                data = text ? { message: text } : {};
+            }
src/main/webapp/WEB-INF/views/common/head.jsp (1)

29-32: 모듈 스크립트와 일반 스크립트 간 실행 순서 주의

type="module" 스크립트는 항상 deferred로 실행됩니다. window.csrfFetch는 문서 파싱 완료 후에 할당되므로, 일반(non-module) 스크립트에서 파싱 시점에 csrfFetch를 호출하면 undefined 에러가 발생합니다.

현재 사용처(이벤트 핸들러, DOMContentLoaded 콜백 등)에서는 문제가 없지만, 향후 일반 스크립트에서 즉시 호출하는 경우 문제가 될 수 있습니다. 이 점을 인지하고 계시면 됩니다.

src/main/webapp/WEB-INF/views/payment/refund-test.jsp (1)

40-43: head.jsp를 포함하지 않아 csrfFetch 임포트가 중복됩니다.

이 페이지는 head.jsp를 include하지 않고 자체 <head>를 사용하고 있어, csrfFetch 모듈 임포트 코드가 중복됩니다. 가능하다면 공통 head.jsp를 include하여 중복을 줄이는 것을 고려해 보세요.

src/main/webapp/resources/common/js/authFetch.js (1)

10-31: csrfFetch.js와 URL 유틸리티 함수 중복.

getBasePath, isAbsoluteHttpUrl, joinBasePath, normalizeInputToUrl, toFetchUrl 등의 함수가 csrfFetch.js에도 동일하게 존재합니다. 공통 모듈로 추출하면 유지보수성이 향상됩니다.

src/main/java/com/flyway/security/jwt/JwtApiAuthFilter.java (1)

93-98: auth.setDetails()에 문자열 상수를 설정하고 있습니다.

Line 96에서 auth.setDetails(JWT_AUTHENTICATED_ATTR)는 "JWT_AUTHENTICATED" 문자열을 설정합니다. 그런데 Line 123에서 Boolean.TRUE.equals(details) 검사는 도달 불가능한 코드입니다. 일관성을 위해 Boolean.TRUE를 사용하거나, 불필요한 검사를 제거하는 것이 좋습니다.

♻️ 일관성 개선 제안
-        auth.setDetails(JWT_AUTHENTICATED_ATTR);
+        auth.setDetails(Boolean.TRUE);

또는 Line 123의 Boolean.TRUE.equals(details) 분기를 제거:

-        return JWT_AUTHENTICATED_ATTR.equals(details) || Boolean.TRUE.equals(details);
+        return JWT_AUTHENTICATED_ATTR.equals(details);
src/test/java/com/flyway/security/filter/CsrfProtectionTest.java (1)

67-76: ArgumentMatchers에 static import 사용 권장

org.mockito.ArgumentMatchers.any()가 줄마다 반복됩니다. 이미 mock, doNothing, verify에는 static import를 사용하고 있으므로 일관성을 위해 ArgumentMatchers.any()도 static import로 정리하면 가독성이 개선됩니다.

♻️ 리팩토링 제안

import 추가:

+import static org.mockito.ArgumentMatchers.any;
-        doNothing().when(authTokenService).refresh(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+        doNothing().when(authTokenService).refresh(any(), any());
-        verify(authTokenService).refresh(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+        verify(authTokenService).refresh(any(), any());
src/main/java/com/flyway/security/config/SecurityConfigWeb.java (1)

93-104: excludePatterns 목록 구성 — 간소화 가능

기능적으로 문제는 없으나, ArrayList + addAll 대신 보다 간결한 방식으로 작성할 수 있습니다.

♻️ 간소화 제안
     `@Bean`
     public RefreshTokenSessionSyncFilter refreshTokenSessionSyncFilter() {
-        List<String> excludes = new ArrayList<>();
-        excludes.addAll(Arrays.asList(STATIC_RESOURCES));
-        excludes.addAll(Arrays.asList(PUBLIC_ENDPOINTS));
+        List<String> excludes = new ArrayList<>(STATIC_RESOURCES.length + PUBLIC_ENDPOINTS.length);
+        Collections.addAll(excludes, STATIC_RESOURCES);
+        Collections.addAll(excludes, PUBLIC_ENDPOINTS);
         return new RefreshTokenSessionSyncFilter(
                 refreshTokenRepository,
                 tokenHasher,
                 authTokenService,
                 excludes
         );
     }
src/main/java/com/flyway/security/filter/RefreshTokenSessionSyncFilter.java (3)

58-69: 인증된 요청마다 DB 조회 발생 — 성능 고려 필요

refreshTokenRepository.findByTokenHash(hash)가 인증된 모든 비공개 경로 요청마다 실행됩니다. 정적 리소스와 공개 엔드포인트는 제외되지만, 트래픽이 많은 인증 페이지에서는 DB 부하가 될 수 있습니다.

세션 속성에 마지막 검증 시각을 저장하고 짧은 TTL(예: 30초~1분) 동안 재검증을 건너뛰는 방식으로 DB 호출을 줄일 수 있습니다.


111-118: 커스텀 경로 매칭 대신 Spring의 AntPathMatcher 사용 권장

현재 matches 메서드는 /** suffix와 정확한 문자열 비교만 지원합니다. excludePatterns에 현재 사용 중인 패턴은 커버되지만, 향후 /*와 같은 단일 레벨 와일드카드가 추가될 경우 동작하지 않습니다.

Spring의 AntPathMatcher를 사용하면 모든 Ant 패턴을 지원하며 Spring Security의 경로 매칭과 일관성을 유지할 수 있습니다.

♻️ AntPathMatcher 적용 제안
+import org.springframework.util.AntPathMatcher;
+
 `@Slf4j`
 `@RequiredArgsConstructor`
 public class RefreshTokenSessionSyncFilter extends OncePerRequestFilter {
 
     private static final String REFRESH_COOKIE = "refreshToken";
+    private static final AntPathMatcher pathMatcher = new AntPathMatcher();
 
     // ...
 
-    private boolean matches(String path, String pattern) {
-        if (pattern == null || pattern.isEmpty()) return false;
-        if (pattern.endsWith("/**")) {
-            String prefix = pattern.substring(0, pattern.length() - 3);
-            return path.startsWith(prefix);
-        }
-        return path.equals(pattern);
-    }
+    private boolean matches(String path, String pattern) {
+        if (pattern == null || pattern.isEmpty()) return false;
+        return pathMatcher.match(pattern, path);
+    }

120-127: readCookie 헬퍼가 AuthTokenServiceImpl과 중복됩니다.

AuthTokenServiceImpl.readCookie와 동일한 로직이 여기에도 존재합니다. 공통 유틸리티로 추출하면 중복을 제거할 수 있습니다.

Comment thread src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java Outdated
Comment thread src/main/java/com/flyway/security/filter/OriginRefererCheckFilter.java Outdated
Comment thread src/main/java/com/flyway/security/jwt/JwtWebAuthFilter.java
Comment thread src/main/webapp/resources/common/js/csrfFetch.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java`:
- Around line 161-181: forceLogout(...) calls this.logout(...) without
`@Transactional` so Spring AOP self-invocation bypasses the transactional proxy
and logout()'s DB call (e.g., refreshTokenRepository.revokeById() invoked from
logout()) may run outside a transaction; fix by annotating forceLogout(...) with
`@Transactional` (importing
org.springframework.transaction.annotation.Transactional if needed) so that
external calls (e.g., from RefreshTokenSessionSyncFilter) get a proper
transactional boundary and logout()'s DB operations are executed within a
transaction.

In `@src/main/java/com/flyway/security/filter/OriginRefererCheckFilter.java`:
- Around line 121-129: isAllowedReferer currently compares the raw referer (only
trimmed) against normalized allowedOrigins, causing case/misc mismatches; update
isAllowedReferer to normalize the incoming referer using the same
normalizeOrigin method (and handle null/empty after trim) before comparing
against allowedOrigins (same checks as in isAllowedOrigin: equality or
startsWith(allowedOrigin + "/")) so case and formatting are consistent between
isAllowedOrigin and isAllowedReferer.

In `@src/main/java/com/flyway/security/jwt/JwtWebAuthFilter.java`:
- Line 32: JWT_AUTHENTICATED_ATTR is declared public but never referenced
externally; change its declaration inside JwtWebAuthFilter from public static
final to private static final to encapsulate it (update the field visibility
only, keep name and value intact).
🧹 Nitpick comments (5)
src/main/java/com/flyway/security/filter/OriginRefererCheckFilter.java (2)

104-110: 로그에 사용자 제어 가능한 헤더 값이 직접 기록되어 로그 인젝션 위험이 있습니다.

Origin과 Referer는 클라이언트가 조작할 수 있는 헤더입니다. 개행 문자(\r, \n) 등이 포함되면 로그 위조(log injection/forging)가 가능합니다. 로그에 기록하기 전에 개행 문자를 제거하거나 치환하는 것이 좋습니다.

🛡️ 제안: 로그 출력 전 헤더 값 새니타이즈
+    private String sanitizeForLog(String value) {
+        if (value == null) return "null";
+        return value.replaceAll("[\\r\\n]", "_");
+    }
+
     if (!allowed) {
         log.warn("[OriginRefererCheck] blocked. method={}, requestURI={}, origin={}, referer={}",
-                request.getMethod(), request.getRequestURI(), origin, referer);
+                request.getMethod(), request.getRequestURI(),
+                sanitizeForLog(origin), sanitizeForLog(referer));

56-72: 팩토리 메서드 forApi()/forWeb()이 정적으로 생성되어 Spring 빈으로 관리되지 않습니다.

SecurityConfigWeb에서 OriginRefererCheckFilter.forWeb()을 호출하여 필터를 생성하고 있어, 이 인스턴스는 Spring 컨텍스트 밖에서 생성됩니다. 현재 코드에서는 외부 의존성이 없어 문제가 되지 않지만, 향후 허용 Origin 목록을 외부 설정에서 주입(@Value 등)하려면 이 구조를 변경해야 합니다. 설정 외부화 시 팩토리 메서드 대신 @Bean 등록 방식으로 전환하는 것을 고려해 주세요.

src/main/webapp/resources/common/js/csrfFetch.js (2)

78-91: ensureCsrfCookie: bootstrap 실패 시 에러가 완전히 무시됩니다.

Line 88의 빈 catch (e) {}가 네트워크 오류를 삼키고 있습니다. csrfFetch Line 117에서 토큰 부재 시 throw하므로 기능적으로는 안전하지만, 디버깅 시 bootstrap 실패 원인을 파악하기 어렵습니다. 최소한 console.warn 정도는 남기는 것을 권장합니다.

♻️ 제안: 경고 로그 추가
-    } catch (e) {}
+    } catch (e) {
+        console.warn("[csrfFetch] CSRF bootstrap failed:", e);
+    }

32-57: URL 유틸리티 함수들이 authFetch.js와 완전히 중복됩니다.

getBasePath, isAbsoluteHttpUrl, joinBasePath, normalizeInputToUrl, toFetchUrl 총 5개 함수가 authFetch.js에도 동일하게 존재합니다. 한 쪽을 수정할 때 다른 쪽도 동기화해야 하는 유지보수 부담이 생깁니다. csrfFetch.js에서 export하고 authFetch.js에서 import하는 방식으로 통합하는 것을 권장합니다.

src/main/webapp/resources/common/js/authFetch.js (1)

10-31: csrfFetch.js와 동일한 URL 유틸리티 5개가 중복 정의되어 있습니다.

getBasePath, isAbsoluteHttpUrl, joinBasePath, normalizeInputToUrl, toFetchUrl이 csrfFetch.js와 완전히 동일합니다. csrfFetch.js에서 이 함수들을 named export하고 여기서 import하면 코드 중복을 제거할 수 있습니다.

♻️ 제안: csrfFetch.js에서 유틸리티 import

csrfFetch.js에서 유틸리티를 export합니다:

export { toFetchUrl, getBasePath, joinBasePath, normalizeInputToUrl, isAbsoluteHttpUrl };

authFetch.js에서 import하여 로컬 정의를 제거합니다:

-import { csrfFetch } from "./csrfFetch.js";
+import { csrfFetch, toFetchUrl } from "./csrfFetch.js";

 const CONFIG = {
     REFRESH_URL: "/auth/refresh",
     LOGIN_URL: "/login",
 };

 let refreshPromise = null;

-function getBasePath() {
-    return window.APP?.contextPath ?? "";
-}
-function isAbsoluteHttpUrl(url) {
-    return typeof url === "string" && /^https?:\/\//i.test(url);
-}
-function joinBasePath(base, path) {
-    if (!base) return path;
-    const b = base.endsWith("/") ? base.slice(0, -1) : base;
-    const p = path.startsWith("/") ? path : `/${path}`;
-    return `${b}${p}`;
-}
-function normalizeInputToUrl(input) {
-    if (input instanceof Request) return input.url;
-    if (typeof input === "string") return input;
-    return String(input);
-}
-function toFetchUrl(input) {
-    const raw = normalizeInputToUrl(input);
-    if (isAbsoluteHttpUrl(raw)) return raw;
-    return joinBasePath(getBasePath(), raw);
-}

Comment thread src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java
Comment thread src/main/java/com/flyway/security/jwt/JwtWebAuthFilter.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java`:
- Around line 100-105: The revokeAllByUserId call inside refresh() is rolled
back when BusinessException is thrown because refresh() is `@Transactional`; to
fix, move the revoke logic into a separate Spring bean method annotated with
`@Transactional`(propagation = Propagation.REQUIRES_NEW) (e.g., create a
RefreshTokenRevocationService with a method revokeAllByUserTokens(userId, now)
that calls refreshTokenRepository.revokeAllByUserId), and replace the direct
repository call in AuthTokenServiceImpl.refresh() with a call to that new
service method so revocation commits immediately even if refresh() rolls back
due to BusinessException.

In `@src/main/resources/config/application-prod.properties`:
- Around line 62-63: The security.allowed-origins property currently only lists
https://flyway.kr which will block requests from https://www.flyway.kr; update
the allowed origins value (property name security.allowed-origins) in
application-prod.properties to include both host variants (e.g., add
https://www.flyway.kr, comma-separated) so OriginRefererCheckFilter will accept
requests from the www subdomain as well.
🧹 Nitpick comments (4)
src/main/java/com/flyway/security/jwt/JwtWebAuthFilter.java (2)

132-133: raw == null 체크는 도달 불가능한 코드입니다.

Line 130의 resolvePath는 request.getRequestURI()(null 반환 없음)를 기반으로 하고, Line 132의 삼항 연산자도 항상 non-null 문자열을 생성합니다. raw == null 조건은 절대 true가 될 수 없습니다. 가독성을 위해 제거하거나, 방어적 코딩 의도라면 그대로 두셔도 무방합니다.


125-139: buildReturnUrl 로직이 세 개의 클래스에서 중복되고 있습니다.

JwtWebAuthFilter, RefreshTokenSessionSyncFilter, WebLoginRedirectEntryPoint 세 클래스 모두에서 거의 동일한 open-redirect 방어 로직이 반복되고 있습니다. 향후 유지보수 시 한쪽만 수정하는 실수를 방지하기 위해, 공통 유틸리티 클래스(예: LoginRedirectUtils)로 추출하는 것을 고려해 주세요.

src/test/java/com/flyway/security/filter/OriginRefererCheckFilterTest.java (1)

16-174: 테스트 커버리지가 양호합니다. 몇 가지 추가 케이스를 고려해 주세요.

주요 시나리오(허용/차단 Origin, Referer 폴백, OPTIONS 우회, 경로 포함/제외, contextPath)가 잘 커버되어 있습니다. 다음 케이스도 추가하면 더 견고해집니다:

  1. GET 요청 우회 테스트 — shouldNotFilter에서 state-changing method가 아닌 요청은 필터를 건너뛰는지 확인
  2. 대소문자 무관 Origin 매칭 — HTTPS://FLYWAY.KR 같은 변형이 통과하는지 확인 (이전 리뷰에서 normalizeOrigin에 toLowerCase 추가를 제안한 바 있음)
src/main/java/com/flyway/security/filter/OriginRefererCheckFilter.java (1)

83-109: API 응답 형식 고려: JSON 에러 바디가 더 적합할 수 있습니다.

API 체인(/api/**)에서도 text/plain 응답("Forbidden")이 반환됩니다. API 클라이언트가 JSON 응답을 기대하는 경우 파싱 오류가 발생할 수 있습니다. 현재 구조에서 API/Web 필터가 같은 클래스를 공유하므로, 필터 유형에 따라 응답 형식을 분기하거나 현재 상태를 유지하되 클라이언트 측에서 403 상태 코드 기반 처리를 하는 것이 좋겠습니다.

Comment thread src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java
Comment thread src/main/resources/config/application-prod.properties Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java (1)

185-188: ⚠️ Potential issue | 🟠 Major

revokeAllRefreshTokens를 RefreshTokenRevocationService로 통합하세요.

이 메서드는 refreshTokenRepository.revokeAllByUserId()를 호출하며, RefreshTokenRevocationService.revokeAllByUserTokens()와 동일한 작업을 수행합니다. 주요 차이점은 트랜잭션 전파 설정입니다:

  • AuthTokenServiceImpl.revokeAllRefreshTokens(): @Transactional (기본값 = REQUIRED)
  • RefreshTokenRevocationService.revokeAllByUserTokens(): @Transactional(propagation = REQUIRES_NEW)

UserWithdrawalServiceImpl.withdraw()에서 현재 이 메서드를 호출하고 있는데, 같은 트랜잭션 내에서 실행되므로 사용자 탈퇴 처리가 롤백되면 토큰 폐기도 함께 롤백됩니다. 이는 데이터 일관성 문제를 야기할 수 있습니다. RefreshTokenRevocationService를 사용하거나, 명시적으로 트랜잭션 전파 전략을 정의하세요.

🧹 Nitpick comments (2)
src/main/java/com/flyway/auth/service/AuthTokenServiceImpl.java (1)

84-86: refresh() 내에서 forceLogout() 자기 호출(self-invocation) 시 트랜잭션 경계 확인 필요

refresh()는 @Transactional이 적용되어 있고, forceLogout()도 @Transactional이지만 동일 빈 내 자기 호출이므로 forceLogout()의 트랜잭션 어노테이션은 프록시를 우회하여 적용되지 않습니다. 이후 BusinessException이 throw되면 refresh()의 트랜잭션이 롤백되어, forceLogout() → logout() 내의 revokeById()도 함께 롤백됩니다.

현재 시나리오에서 실질적인 보안 문제는 제한적입니다:

  • 재사용 탐지 (Line 103): refreshTokenRevocationService.revokeAllByUserTokens()가 REQUIRES_NEW로 별도 커밋되므로 안전
  • 기타 에러 경로: 이미 무효/만료/누락된 토큰이므로 revokeById 롤백의 실질적 영향 미미
  • 쿠키 삭제/세션 무효화: HTTP 응답 작업이므로 DB 롤백과 무관하게 정상 수행

다만, 향후 logout() 로직이 복잡해질 경우를 대비해 이 동작을 인지하고 있는 것이 중요합니다.

Also applies to: 91-93, 97-99, 103-106, 134-136

src/test/java/com/flyway/auth/service/AuthTokenServiceImplTest.java (1)

100-123: 재사용 토큰 시나리오 테스트가 핵심 보안 로직을 잘 검증합니다.

rotatedAt이 설정된 토큰으로 재사용 탐지 경로를 테스트하고, REQUIRES_NEW 트랜잭션의 revocation 서비스 호출과 예외 발생을 모두 검증합니다.

추가적으로 forceLogout의 부수 효과(쿠키 삭제, 세션 무효화)도 검증하면 커버리지가 더 강화됩니다. 예를 들어:

// forceLogout으로 인한 쿠키 삭제 검증
List<String> cookies = response.getHeaders(HttpHeaders.SET_COOKIE);
assertThat(cookies).anyMatch(c -> c.contains("accessToken=;") || c.contains("accessToken=\n"));
assertThat(cookies).anyMatch(c -> c.contains("refreshToken=;") || c.contains("refreshToken=\n"));

@gaeunnlee gaeunnlee added FEAT 새로운 기능 추가 또는 기존 기능 확장 ready-for-review PR 리뷰 요청 labels Feb 13, 2026

@hjh79gw hjh79gw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

csrf 설정 고생하셨습니다

@ochanhyeok ochanhyeok left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!!

@ochanhyeok
ochanhyeok merged commit 4791c1d into dev Feb 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FEAT 새로운 기능 추가 또는 기존 기능 확장 ready-for-review PR 리뷰 요청

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FLYW-140] CSRF 설정 분리 (JSP Web 활성화 / API Origin 검증 적용)

3 participants