앞서 SPA 환경에 적합한 CSRF 토큰 저장 전략으로 CookieCsrfTokenRepository를 선택했다.
토큰을 저장하는 것만큼 토큰을 처리하는 방식도 중요하다. Spring Security의 CsrfTokenRequestHandler를 커스터마이징하여, SPA 환경에 최적화된 SpaCsrfTokenRequestHandler를 구현하는 과정을 정리한다.
CsrfTokenRequestHandler의 역할
CsrfTokenRequestHandler는 CSRF 토큰과 관련된 요청(Request)과 응답(Response)을 처리하는 책임을 가진다.
- 요청 처리 클라이언트가 보낸
HttpServletRequest에서 CSRF 토큰을 어떻게 읽어올 것인가(resolve) 를 결정한다. - 응답 처리
HttpServletResponse에 CSRF 토큰을 어떻게 포함시킬 것인가(handle) 를 결정한다.
SpaCsrfTokenRequestHandler 상세 분석
SpaCsrfTokenRequestHandler는 두 개의 기본 핸들러(plain, xor)의 장점을 결합하여 SPA 환경의 요구사항을 충족시키는 커스텀 핸들러다.
전략 패턴(Strategy Pattern)
SpaCsrfTokenRequestHandler는 두 명의 전문가(plain, xor)에게 실제 작업을 위임한다. 상황(요청 헤더 유무)에 따라 적절한 전문가(전략)를 선택하여 사용하는 구조는 전략 패턴의 좋은 예시다.
// SpaCsrfTokenRequestHandler.java
public class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
// ...
}
이 클래스의 핵심 메서드들을 하나씩 살펴보자.
resolveCsrfTokenValue()
클라이언트 요청에서 CSRF 토큰을 찾아내는 역할을 한다.
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
return (StringUtils.hasText(headerValue) ? this.plain : this.xor)
.resolveCsrfTokenValue(request, csrfToken);
}
request.getHeader(...)X-XSRF-TOKEN헤더에 값이 있는지 먼저 확인한다. SPA가 보내는 API 요청의 표준 방식이다.? this.plain : this.xor전략 패턴의 핵심이다. 헤더에 값이 있으면 헤더 읽기 전문가plain에게, 없다면_csrf파라미터 등을 찾을 수 있는xor전문가에게 작업을 위임한다.
handle()
생성된 토큰을 어떻게 안전하게 클라이언트에게 전달할지 결정한다.
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
this.xor.handle(request, response, csrfToken);
csrfToken.get();
}
this.xor.handle(...)응답 본문에 토큰이 포함될 경우, 토큰을 랜덤 값과 XOR 연산하여 마스킹한다. BREACH 공격을 방어하기 위한 핵심 보안 조치다.csrfToken.get()지연 로딩(Lazy Loading)되는 토큰을 실제로 생성/로드하여,CookieCsrfTokenRepository가Set-Cookie헤더를 응답에 추가하도록 하는 결정적인 트리거 역할을 한다.
SecurityConfig에 커스텀 핸들러 적용
// SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // 커스텀 핸들러 등록
);
return http.build();
}
* 커스텀 핸들러 미등록
SpaCsrfTokenRequestHandler.java 파일을 만들어 놓고, SecurityConfig의 .csrfTokenRequestHandler()에 등록하는 것을 잊는 경우가 많다. 등록하지 않으면 Spring Security는 기본 핸들러를 사용하게 되고, 의도한 '헤더 우선 검증' 로직이 동작하지 않는다.
* csrfToken.get() 누락
handle() 메서드에서 이 코드를 빼먹으면 토큰이 활성화되지 않아 클라이언트는 토큰 쿠키를 받지 못하고, 이후의 모든 요청은 CSRF 검증에 실패한다.
다음 글에서는 이 모든 설정의 마지막 퍼즐 조각인 클라이언트가 최초에 CSRF 토큰을 발급받기 위한 API 엔드포인트 구현을 다룬다.