CSRF 토큰의 저장소와 처리기를 모두 SPA 환경에 맞게 설정했다. 서버는 클라이언트의 요청을 검증할 준비를 마친 셈이다.
그런데 한 가지가 빠졌다. 클라이언트, 즉 SPA는 어떻게 최초의 CSRF 토큰을 얻을 수 있을까? 이 마지막 퍼즐 조각인 토큰 발급 API 엔드포인트를 구현하고, "Double Submit Cookie" 패턴의 전체 흐름을 정리한다.
왜 SPA는 전용 토큰 발급 API가 필요한가
SPA는 최초에 index.html과 같은 정적 리소스를 로드한 후, 자바스크립트를 통해 동적으로 페이지를 그려나간다. 이 과정에서 CSRF 토큰이 자동으로 발급된다고 보장할 수 없다.
따라서 SPA가 모든 로딩을 마친 후, "CSRF 토큰을 발급해 달라"고 명시적으로 요청할 수 있는 공식적인 창구가 필요하다.
API 구현 및 'Double Submit Cookie' 패턴의 완성
AuthController.java에 토큰 발급 API를 구현한다.
// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@GetMapping("csrf-token")
public ResponseEntity<Void> getCsrfToken(CsrfToken csrfToken) {
return ResponseEntity.noContent().build();
}
}
* GET 메서드
토큰을 '획득'하는 행위는 리소스의 상태를 변경하지 않는 안전하고 멱등적인(idempotent) 작업이므로, GET을 사용하는 것이 HTTP 시맨틱에 부합한다.
* 204 No Content
이 API의 핵심 결과물은 응답 본문이 아니라, 응답 헤더에 Set-Cookie를 포함시키는 부수 효과(Side effect)다. "요청은 성공했지만, 본문에 보낼 콘텐츠는 없다"는 의미를 명확히 전달하는 204 No Content가 시맨틱적으로 더 정확한 응답이다.
'Double Submit Cookie' 패턴 최종 흐름도
JS가 이 쿠키 값을 읽어 변수에 보관 SPA 클라이언트->>서버: 4. POST /api/messages (헤더: X-XSRF-TOKEN, 쿠키: XSRF-TOKEN) 서버->>서버: 5. 헤더의 토큰과 쿠키의 토큰을 비교 alt 검증 성공 서버-->>SPA 클라이언트: 200 OK (요청 처리) else 검증 실패 서버-->>SPA 클라이언트: 403 Forbidden (요청 거부) end
심층 분석 - Spring MVC의 HandlerMethodArgumentResolver
getCsrfToken(CsrfToken csrfToken) 처럼, 컨트롤러 메서드 파라미터를 선언하는 것만으로 어떻게 필요한 객체가 자동으로 주입될까? Spring MVC의 ==HandlerMethodArgumentResolver== 라는 메커니즘 덕분이다.
- 역할 '인자 해석기'. 컨트롤러 메서드가 호출되기 전에, 메서드가 필요로 하는 파라미터들을 해석하고 준비해주는 역할을 한다.
- 동작 Spring Security는
CsrfToken타입의 파라미터를 위한CsrfTokenArgumentResolver를 사전에 등록해 두었다. 이 해석기는 요청이 들어오면CsrfToken파라미터를 인지하고,CsrfTokenRepository를 통해 토큰 객체를 가져오거나 생성하여 메서드에 주입해 준다.
* CORS 정책 설정 누락
프론트엔드와 백엔드의 출처(Origin)가 다른 경우, 백엔드의 WebMvcConfigurer에서 /api/auth/csrf-token 경로에 대한 접근을 허용하도록 CORS 정책을 설정해야 한다. 그렇지 않으면 브라우저가 보안상의 이유로 API 요청 자체를 차단한다.
* 클라이언트의 API 호출 순서 오류
SPA 클라이언트는 반드시 상태를 변경하는 다른 API(예: 로그인, 글쓰기)를 호출하기 전에 토큰 발급 API를 먼저 호출해야 한다. 순서가 틀리면, CSRF 토큰이 없는 상태에서 API를 호출하게 되어 403 Forbidden 오류를 만나게 된다.
* 클라이언트의 헤더 이름 오타
Spring Security의 기본 헤더 이름은 X-XSRF-TOKEN이다. 클라이언트의 Axios 인터셉터 등에서 이 헤더 이름을 X-CSRF-TOKEN 등으로 잘못 설정하면 서버는 헤더를 찾지 못해 요청을 거부한다.
이것으로 총 4편에 걸친 Spring Security CSRF 방어 설정 시리즈를 마친다.
Q. SPA 환경에서 Spring Security를 사용하여 CSRF 공격을 방어하는 전체 과정을 설명해 보세요.
A. SPA 환경에서는 서버가 상태를 유지하지 않는 Stateless 방식이 유리하므로, 'Double Submit Cookie' 패턴을 기반으로 CSRF 방어를 구현한다.
1. 먼저 CsrfTokenRepository의 구현체로 CookieCsrfTokenRepository를 선택하여 CSRF 토큰을 클라이언트 쿠키에 저장하도록 설정한다. 이때 자바스크립트가 토큰을 읽을 수 있도록 withHttpOnlyFalse() 옵션을 사용한다.
2. 다음으로, SPA 환경에 최적화된 커스텀 CsrfTokenRequestHandler를 구현한다. 이 핸들러는 요청이 들어올 때 X-XSRF-TOKEN 헤더를 우선적으로 확인하고, 응답 시에는 BREACH 공격 방어를 위해 토큰을 마스킹하는 역할을 수행한다.
3. 마지막으로, SPA 클라이언트가 최초에 토큰을 발급받을 수 있도록 GET 방식의 API 엔드포인트를 구현한다. 클라이언트는 이 API를 호출하여 토큰 쿠키를 발급받고, 이후 모든 상태 변경 요청에 토큰을 헤더와 쿠키에 담아 전송하여 서버의 이중 검증을 통과하게 된다.