CSRF (Cross-Site Request Forgery)
- 웹 애플리케이션의 취약점을 악용해 사용자의 권한을 도용하여 악의적인 요청을 보내는 공격을 방지하기 위한 보안 기능
- Spring Security는 CSRF 공격을 방지하는 기본적인 메커니즘을 제공하며 CSRF 보호는 Spring Security를 사용하는 대부분의 애플리케이션에서 기본적으로 활성화되어 있음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
public class SecurityConfig { | |
// 중략 | |
@Bean | |
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { | |
httpSecurity.httpBasic(Customizer.withDefaults()); | |
// 중략 | |
httpSecurity.csrf(AbstractHttpConfigurer::disable); // HTTP POST 방식으로 엔드포인트를 호출할 수 있도록 CSRF 비활성화 | |
return httpSecurity.build(); | |
} | |
} |
CSRF 공격
- 사용자가 자신도 모르게 공격자의 의도된 작업을 수행하게 만드는 공격 기법
- 사용자의 의지와는 무관하게 공격자가 의도한 행동을 수행해서 웹 페이지의 보안을 취약하게 하거나 수정/삭제 등의 작업을 하는 공격 방법
- ex) Man in the Middle 공격을 통해 인증된 세션을 탈취하고 해당 세션을 기반으로 서버에 악성 요청을 보냄
Spring Security에서의 CSRF 보호
- 스프링 시큐리티는 CSRF 공격을 방지하기 위해 사용자가 신뢰할 수 있는 요청을 구분하는 메커니즘 사용
- 요청의 정당성을 검증하기 위해 CSRF 토큰 사용
- 서버는 클라이언트에게 CSRF 토큰을 발급하고
- 클라이언트는 해당 토큰을 함께 전송함으로써 서버에 자신의 요청이 신뢰할 수 있는 요청임을 증명
1. CSRF 보호가 작동하는 방식
- 토큰 발급: 사용자가 GET 요청으로 웹 애플리케이션에 처음 접근할 때 서버는 클라이언트에게 CSRF 토큰을 발급하고 해당 토큰은 보통 HTML의 hidden 필드나 HTTP 헤더에 포함됨
- 토큰 전송: 클라이언트는 POST, PUT, DELETE 같은 상태를 변경하는 요청을 보낼 때 CSRF 토큰을 함께 전송하고 서버는 해당 토큰을 검증하여 요청이 정당한지 검증
- 서버 검증: 서버는 클라이언트로부터 받은 토큰과 서버에 저장된 토큰을 비교하여 요청이 위조되지 않았는지 검증하고 토큰이 일치하지 않을 경우 요청 거부

2. CsrfFilter
- 스프링 시큐리티에서 제공하는 CsrfFilter에서 CSRF 보호를 수행함
- CsrfFilter는 하나의 클래스로 OncePerRequestFilter를 확장
- RequestMatcher, CsrfTokenRequestHandler 등이 포함되어 있는 것을 확인할 수 있음
- 해당 필터는 요청을 가로채서 GET, HEAD, TRACE, OPTIONS의 HTTP 요청은 모두 허용하지만 그 외 HTTP 요청은 토큰이 포함된 헤더가 있는지 확인하는 과정을 거침
- 만약 헤더에 올바른 토큰 값이 없거나, 아예 헤더가 없는 경우 403 Forbidden으로 응답을 내려줌
- 토큰은 하나의 문자열 값으로 GET, HEAD, TRACE, OPTIONS 요청 이외의 요청에 대해서는 헤더에 해당 토큰을 포함해서 요청해야 함

- CsrfTokenRepository를 이용하여 새로운 토큰을 생성 및 저장하는 등 CSRF 토큰 값을 관리
- 토큰은 UUID 값으로 생성
- 토큰을 HTTP 세션에 저장

3. 코드 예시
- 모든 요청이 인증을 요구하도록 설정
- 로그인을 성공하면 루트 페이지로 리다이렉트되도록 설정
- form 내 hidden input을 추가하여 POST 요청에 CSRF 토큰을 같이 전달하도록 설정
- 로그인에 성공하면 hidden input의 value에 CSRF 토큰 값이 설정됨
3.1 SecurityConfig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
public class SecurityConfig { | |
@Bean | |
public UserDetailsService userDetailsService() { | |
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(); | |
userDetailsManager.createUser(User.withUsername("jaimemin") | |
.password("1234") | |
.build()); | |
return userDetailsManager; | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
@Bean | |
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { | |
httpSecurity.authorizeHttpRequests(c -> c.anyRequest().authenticated()); | |
httpSecurity.formLogin(c -> c.defaultSuccessUrl("/", true)); | |
return httpSecurity.build(); | |
} | |
} |
3.2 html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>CSRF 토큰 테스트</title> | |
</head> | |
<body> | |
<form action="/add" method="post"> | |
<span>Name:</span> | |
<span><input type="text" name="name"/></span> | |
<span><button type="submit">add</button></span> | |
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> | |
</form> | |
</body> | |
</html> |

3.3 특정 경로에만 CSRF 보호를 적용하고 싶을 경우
- csrf()의 ignoringRequestMatchers를 활용하여 보호를 제외하고 싶은 경로 입력
- /api/** 경로의 POST api에 대해서는 200 OK
- 그 외 경로의 POST api에 대해서는 403 Forbidden



CORS (Cross-Origin Resource Sharing)
- 웹 애플리케이션이 다른 도메인에서의 요청을 허용하는 방식과 관련된 보안 설정
- 기본적으로 웹 브라우저는 보안 정책인 동일 출처 정책(Same-Origin Policy)에 따라, 서로 다른 도메인 간의 리소스 요청을 제한함
- 그러나 특정 상황에서는 다른 도메인에서 리소스에 접근해야 할 때가 있으며, 이를 위해 CORS가 사용
- ex) 백엔드와 프론트엔드가 별도의 애플리케이션으로 개발이 되는 경우에 이를 적용 (SpringBoot, React)

1. CORS 동작 방식
- CORS는 클라이언트와 서버 간의 상호작용을 제어하는 일련의 HTTP 헤더로 구성됨
- 웹 애플리케이션이 다른 도메인으로부터 리소스를 요청할 때 서버는 브라우저가 해당 요청을 수용할지 여부를 결정할 수 있는 정보 제공
- CORS는 기본적으로 두 가지 종류의 요청을 다룸
- Simple Request
- Preflight Request
1.1 Simple Request
- GET, HEAD, POST 중 하나의 메서드로 수행되는 요청
- POST 요청일 경우 `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain` 등의 Content-Type 헤더만 사용 가능
- Simple Request는 바로 서버로 전송되며 서버는 응답의 헤더에 `Access-Control-Allow-Origin` 등을 포함하여 해당 요청의 허용 여부를 브라우저에 알림
1.2 Preflight Request
- PUT, DELETE 같은 메서드, 커스텀 헤더가 포함된 요청, `application/json` 등 특별한 Content-Type이 있을 경우에는 브라우저가 실제 요청을 보내기 전 Preflight Request를 보냄
- Preflight Request는 OPTIONS 메서드로 서버에 보내지며 서버는 이에 응답해 해당 요청의 허용 여부를 브라우저에 알려줌

2. CORS 관련 헤더
- HTTP 헤더를 기반으로 작동하며 중요한 헤더로는 다음의 3개의 헤더가 존재함
- Access-Control-Allow-Origin: 서버가 해당 요청을 허용할 수 있는 출처(origin)를 명시, 브라우저는 해당 값이 클라이언트의 출처와 일치할 때만 요청을 허용
- Access-Control-Allow-Methods: 서버가 허용하는 HTTP 메서드를 지정 ex) GET, POST, PUT, DELETE
- Access-Control-Allow-Headers: 클라이언트가 보내는 요청에 포함될 수 있는 헤더들을 명시
* 주의: Spring Security는 이러한 헤더를 응답에 기본적으로 추가하지 않기 때문에 별도로 추가해줘야 함
3. 코드 예시
- CORS 관련 테스트에 집중하기 위해 CSRF는 비활성화하며 모든 요청을 허용하도록 처리
- 브라우저에서는 localhost:8080으로 접근하고 javascript에서는 127.0.0.1:8080으로 api를 찔러 응답 값을 화면에 노출하도록 설정
- SecurityConfig에서 별도로 setAllowedOrigins, setAllowedMethods 설정 유무에 따른 차이를 확인 가능
3.1 SecurityConfig
- CORS 설정 확인을 위해 CorsConfigurer을 통해 setAllowedOrigins, setAllowedMethods 설정한 부분을 주석 처리
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
public class SecurityConfig { | |
@Bean | |
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { | |
httpSecurity.csrf(AbstractHttpConfigurer::disable); | |
httpSecurity.authorizeHttpRequests(c -> c.anyRequest().permitAll()); | |
httpSecurity.cors(c -> { | |
CorsConfigurationSource source = request -> { | |
CorsConfiguration config = new CorsConfiguration(); | |
// config.setAllowedOrigins( | |
// List.of("http://localhost:8080") | |
// ); | |
// config.setAllowedMethods( | |
// List.of("GET", "POST", "PUT", "DELETE") | |
// ); | |
return config; | |
}; | |
c.configurationSource(source); | |
}); | |
return httpSecurity.build(); | |
} | |
} |
3.2 html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<script> | |
const http = new XMLHttpRequest(); | |
const url = 'http://127.0.0.1:8080/test'; | |
http.open("POST", url); | |
http.send(); | |
http.onreadystatechange = (e) => { | |
document.getElementById("output") | |
.innerHTML = http.responseText; | |
} | |
</script> | |
</head> | |
<body> | |
<div id="output"> | |
</div> | |
</body> | |
</html> |

부연 설명
- Access-Control-Allow-Origin HTTP 헤더를 설정하는 부분이 주석처리 되어있기 때문에 응답이 수락되지 않음
- 별도의 설정이 없을 경우 SpringBoot는 CORS 헤더를 설정하지 않음
- 교차 출처 호출을 막아두기 때문에 브라우저에서는 아무것도 노출이 안 되는 것이 맞음
- 주석을 해체할 경우 정상적으로 동작하는 것을 확인 가능
3.3 CorsConfigurer 외 CORS 허용하는 방법
- @CrossOrigin 어노테이션을 활용하여 여러 출처를 정의하는 배열을 입력 가능
- 모든 출처를 허용하도록 *를 활용할 수도 있지만 이렇게 설정할 경우 XSS(교차 사이트 스크립팅) 요청에 노출되어 보안 취약점들이 많아짐
- 엔드 포인트를 정의하는 곳에서 @CrossOrigin을 활용하면 명시적이긴 하지만
- 동일한 코드가 반복되고
- 코드 복잡성이 올라가고
- 개발자가 새로 구현하는 엔드포인트에 대해 매번 적용해줘야하는 문제점 발생
- 따라서 CorsConfigurer을 통해 CORS를 허용하는 것을 권장

참고
반응형
'Spring > Spring Security' 카테고리의 다른 글
[9장] 필터 구현 (0) | 2025.05.31 |
---|---|
[Spring Security] 전역 메서드 보안 (0) | 2024.10.09 |
[Spring Security] 필터 체인과 커스텀 필터 (0) | 2024.10.08 |
[Spring Security] 액세스 제한과 권한 (0) | 2024.10.07 |
[Spring Security] 아키텍처 간단 정리 (0) | 2024.09.13 |