CSRF란 Cross-Site Request Forgery의 준말로 사이트 간 요청 위조라는 의미이다. 말 그대로는 어떤 의미인지 잘 안 와닿을 수도 있는데 예시를 들자면 A라는 사이트에 로그인한 어떤 사용자가 다른 사이트에서 접속하여 악성 스크립트를 자기도 모르는 새에 실행하게 된다고 했을 때 A 사이트에 마치 해당 사용자가 직접 실행한 것처럼 서버에 요청하는 것이다. 그래서 사용자가 원치 않는 행위를 하도록 하는 것이다. 이런 일이 일어나지 않도록 CSRF 보호를 적용하는 것이다. 스프링 시큐리티를 사용하면 기본적인 기능은 쉽게 구현할 수 있다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 코드 생략
.csrf(AbstractHttpConfigurer::disable)
// 람다식 사용 .csrf(c -> c.disable())
.build();
}
테스트 환경에서는 위의 코드처럼 csrf 보호를 비활성화해서 사용할 것이다. 왜냐하면 GET 방식으로는 그냥 사용할 수 있지만 POST, PUT, PATCH와 같은 메서드를 사용할 때 CSRF 토큰 인증을 못해서 필터 단에서 걸러지기 때문이다. 그렇기 때문에 Postman과 같은 프로그램을 사용해서 POST 요청을 보내면 403 forbidden 응답이 반환될 것이다.
이를 해결하기 위해서는 CSRF 보호를 활성화 시켜야 하는데 기본으로 아무것도 작성하지 않으면 활성화이니 간단하게 비활성화한 코드가 있다면 이를 주석처리하거나 삭제하면 된다.
스프링 시큐리티에서 CSRF 보호를 하는 과정은 다음과 같다.
- 클라이언트에서 서버로 GET 요청을 보낸다.
- 로그인하지 않은 상태이면 자동으로 AnonymousAuthenticationToken을 생성하여 익명 세션을 만들어 저장한다.
- 익명 세션도 Authentication 구현체의 객체이기 때문에 CSRF 보호를 적용하면서 실행하려면 서버에서 발급해준 CSRF 토큰이 필요하다.
- 이후에 요청 헤더에 X-CSRF-TOKEN의 값으로 서버에서 발급한 CSRF 토큰을 직접 입력하여 서버로 전달하거나, form의 input 태그 hidden 값으로 서버로부터 응답받은 CSRF 토큰을 _csrf name으로 다시 전달하는 식으로 자신이 정상적인 스크립트 상에서 동작하고 있다는 것을 서버에 알린다.
- 로그인 시도를 하는데 아이디와 비밀번호가 맞고 CSRF 토큰이 이전에 발급했던 값과 동일하다면 정상적으로 동작할 것이고 세가지 정보 중 하나라도 다르면 로그인하지 못할 것이다.
이제 직접 테스트해서 동작 과정을 살펴보자.
@Slf4j
public class CsrfLoggerFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Object csrf = request.getAttribute("_csrf");
CsrfToken csrfToken = (CsrfToken) csrf;
log.info("Request URI = {}", request.getRequestURI());
log.info("현재 sessionID = {}", request.getSession().getId());
log.info("CSRF Token getParam = {}, getHeader = {} , getToken = {}",
csrfToken.getParameterName(), csrfToken.getHeaderName(), csrfToken.getToken());
filterChain.doFilter(request, response);
}
}
위 필터는 CsrfFilter 바로 뒤에서 실행되도록 하여 CsrfFilter에서 생성한 CSRF 토큰의 정보를 출력한다. 직접 어느 경로에 접속해보면 다음과 같은 결과를 알 수 있다.

http://localhost:8080/login 으로 접속했더니 여러 정보가 나온다. 나의 클라이언트 세션 ID와 CSRF 정보 등을 알 수 있다. 폼으로 토큰을 전송할 때는 _csrf로, 헤더로 전송할 때는 X-CSRF_TOKEN으로 전송하도록 설정되어 있다. 밑의 익명 정보는 컨트롤러에서 출력한 정보이므로 신경쓰지 말자.
formLogin 방식으로 로그인을 시도한다. 일단 그냥 POST 메서드 데이터로 email과 password를 포함시켜 요청해보자. username이 아니라 email인 이유는 그냥 내가 설정에서 바꿨기 때문이다... ㅇ.ㅇ

결과가 뭔가 이상하다. 로그인이 되지 않은 것이다. CSRF 토큰이 없어서 제대로 작동하지 않은 것이라고 예상해볼 수 있다. 서버 쪽 콘솔 로그를 확인해보자.

클라이언트 상의 JSESSIONID와 현재 sessionID가 같다. 즉 Postman에서 보낸 요청에 대한 정보라는 것을 알 수 있다. 이제 밑의 토큰 정보를 가지고 다시 로그인을 시도해 보자.

헤더에 X-SCRF-TOKEN 정보를 포함시켜 요청했더니 이제 정상적으로 로그인 됐다!
이제 로그아웃을 시도해 보자. 로그아웃에도 CSRF 토큰이 필요하도록 POST 메서드로 처리했다.

403 fobidden이 응답됐다. CSRF 토큰이 없어서 요청이 거부된 것이다. CSRF 토큰을 포함시켜서 다시 요청해보자.

CSRF 토큰의 값이 달라져서 다시 새로운 값을 사용해야 한다. CSRF 토큰은 기본적으로 한 요청에 한 개, 동일 세션에서는 동일한 토큰을 사용한다고 알고 있었는데 뭔가 다른 방식이 있는 것 같다. 사실 이 부분은 잘 모르겠다...

CSRF 토큰을 포함시켰더니 정상적으로 로그아웃 되었다.

'공부 > Spring Security' 카테고리의 다른 글
| [Spring Security] JWT Deprecated 해결하기 (0) | 2024.03.18 |
|---|---|
| [Spring Security] Filter 인증 구조에 대해서 깨달은 점 (0) | 2024.01.17 |
| [Spring Security] 원하는 Filter 만들어서 사용하기 (0) | 2024.01.09 |
| [Spring Security] mvcMatchers, antMatchers, regexMatchers 대신 requestMatchers (0) | 2024.01.08 |
| [Spring Security] AuthenticationProvider 직접 구현하기 (0) | 2024.01.04 |