[Spring Security] CSRF ์ค์ 403 ์๋ต ๋ฌธ์ ํด๊ฒฐ: CsrfTokenRepository์ ๋ํ ์ดํด
โ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ
Spring Boot 3.4.0
Spring Security 6.4.1
๐ ๋ฌธ์ ์ํฉ
๋์ค์ฝ๋์ ๋ฏธ์ ์ Spring Security๋ฅผ ์ ์ฉํ๋ฉฐ CSRF ๋ณดํธ ์ค์ ์ ํ๊ณ ์์์ต๋๋ค.
์ด ํ๋ก์ ํธ์ ํ๋ก ํธ์๋๋ ์๋ฒ์์ HTML์ ๋ชจ๋ ์์ฑํ๋ SSR(Sever Side Rendering) ๋ฐฉ์์ด ์๋๋ผ,
ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )์์ Javascript๋ฅผ ํ์ฉํด HTML์ ์์ฑํ๋ CSR(Client Side Rendering) ๋ฐฉ์์ ๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ CSRF ํ ํฐ ์ ๋ณด ์ญ์ ํ๋ก ํธ์์ ๋ช ์์ ์ผ๋ก ๊ด๋ฆฌํด์ผ ํ์ต๋๋ค.
CSRF ํ ํฐ ๋ฐ๊ธ ๋ฐ ๊ฒ์ฆ ํ๋ก์ธ์ค๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ์ ๋ ์ด ์๊ตฌ์ฌํญ์ ๋ง์ถฐ ๋ฐฑ์๋๋ฅผ ๊ตฌํํด์ผ ํ์ต๋๋ค.
1. ํ์ด์ง๊ฐ ๋ก๋๋ ๋ ์๋ฒ๋ก๋ถํฐ CSRF ํ ํฐ ๋ฐ๊ธ
2. CSRF ํ ํฐ์ ์ฟ ํค(CSRF-TOKEN)์ ์ ์ฅ
3. ๋งค ์์ฒญ๋ง๋ค ์ฟ ํค์ ์ ์ฅ๋ CSRF ํ ํฐ์ ํค๋(X-CSRF-TOKEN)์ ํฌํจ
ํ๋ก ํธ์๋์์ CSRF ํ ํฐ์ ๋ฐ๊ธ๋ฐ๊ธฐ ์ํด GET /api/auth/csrf-token API๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
@GetMapping("/csrf-token")
public ResponseEntity<CsrfToken> getCsrfToken(CsrfToken csrfToken) {
return ResponseEntity.ok(csrfToken);
}
๊ทธ๋ฆฌ๊ณ SecurityConfig์์ csrf ์ค์ ์ ํ์ต๋๋ค.
ํ ํฐ์ ์ฟ ํค์ ์ ์ฅํด์ผ ํ๋ฏ๋ก ํ ํฐ ์ ์ฅ์๋ CookieCsrfTokenRepository๊ฐ ๋์ด์ผ ํ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain chain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(auth -> auth
// ์ ์ ๋ฆฌ์์ค ์์ฒญ ํ์ฉ
.requestMatchers("/assets/**", "/favicon.ico", "/index.html").permitAll()
.requestMatchers("/", "/error/*").permitAll()
// Swagger ์์ฒญ ํ์ฉ
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html",
"/webjars/**").permitAll()
// Actuator ์์ฒญ ํ์ฉ
.requestMatchers("/actuator/**").permitAll()
// CSRF ํ ํฐ์ ๋ฐ๊ธํ๋ API ์์ฒญ ํ์ฉ
.requestMatchers("/api/auth/csrf-token").permitAll()
// ํ์๊ฐ์
API ์์ฒญ ํ์ฉ
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
// ๊ทธ ์ธ ์ธ์ฆ ํ์
.anyRequest().authenticated())
// ๐ ์ฌ๊ธฐ!! -----------------------------------------------------------------------
.csrf(csrf -> csrf
// JavaScript์์ ์ฟ ํค๋ฅผ ์ฝ์ ์ ์๊ฒ ํจ
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
// ---------------------------------------------------------------------------------
// LogoutFilter ์ ์ธ
.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.disable())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return httpSecurity.build();
}
์คํ ๊ฒฐ๊ณผ
์ฐ์ /api/auth/csrf-token ์์ฒญ์ผ๋ก ํ๋ก ํธ์์๋ CSRF ํ ํฐ ๊ฐ์ ๊ฐ์ ธ๊ฐ๋๋ค.
๊ทธ ๋ค์ POST ์์ฒญ์ ๋ํด 403 ์๋ต์ด ์ต๋๋ค.
์ด๋ Request Header๋ฅผ ๋ณด๋ฉด ์ฟ ํค์ ํค๋์ CSRF ํ ํฐ์ด ๋ชจ๋ ์ ๋๋ก ๋ค์ด์์ต๋๋ค.
๊ทธ๋ฌ๋ ์๋ฒ ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด "Invalid CSRF token"์ด๋ผ๋ฉฐ CSRF ๊ฒ์ฆ ๊ณผ์ ์ ์คํจํ์ต๋๋ค.
๋ฆฌํ์คํธ ํค๋์ ์ฟ ํค ๋ชจ๋์ ์ ๋๋ก ๋ CSRF ํ ํฐ์ ๊ฐ์ง๊ณ ์๋ ๊ฒ ๊ฐ์๋ฐ ์์ผ๊น์?
๋๋ฒ๊น : CsrfFilter ๋ด๋ถ ๋์
๋๋ฒ๊น ๋ชจ๋๋ก ๋ค์ ํ ๋ฒ ์์ฒญ์ ๋ ๋ ค๋ด ์๋ค.
์ด๋ฒ์๋ ๋ค์๊ณผ ๊ฐ์ด Request๋ฅผ ๋ณด๋ด๊ณ ์์ต๋๋ค.
CsrfFilter์์ doFilterInternal() ๋ฉ์๋์ ์ค๋จ์ ์ ๊ฑธ์์ต๋๋ค.
์๊น ์ฝ์ ๋ก๊ทธ์์ "Invalid CSRF token..."์ ๋ดค์ผ๋ ์ฐ๋ฆฌ๋ else๋ฌธ์ ๋ด์ผ ํฉ๋๋ค.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
CsrfTokenRequestHandler var10000 = this.requestHandler;
Objects.requireNonNull(deferredCsrfToken);
var10000.handle(request, response, deferredCsrfToken::get);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
} else {
CsrfToken csrfToken = deferredCsrfToken.get();
String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
boolean missingToken = deferredCsrfToken.isGenerated();
this.logger.debug(LogMessage.of(() -> {
return "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request);
}));
AccessDeniedException exception = !missingToken ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
} else {
filterChain.doFilter(request, response);
}
}
}
์ด๋ ์ ์์์ผ๋ก๋
์๋ฒ์ ํ ํฐ ์ ์ฅ์์์ ๊ฐ์ ธ์จ csrfToken ๊ฐ๊ณผ ๋ฆฌํ์คํธ์์ ๋ณด๋ธ actualToken ๊ฐ์ด ๊ฐ์์ผ ํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ actualToken ๊ฐ์ null์ด๊ณ , csrfToken ๊ฐ์ ์ ๊ฐ ์๊ฐ๊ณผ ๋ฌ๋ฆฌ
"X-CSRF-TOKEN" ํค๋๊ฐ ์๋๋ผ "X-XSRF-TOKEN"์ ํด๋นํฉ๋๋ค!
๊ทธ๋ฌ๊ณ ๋ณด๋ ์๊น CSRF ๋ฐ๊ธ ์์ฒญ์์..
์๋ต์์ headerName์ด X-XSRF-TOKEN์ด๋ผ ์ ํ์๋ค์.
์ CSRF๊ฐ ์๋๋ผ XSRF๋ผ๊ณ ๋์ด ์๋๊ฑธ๊น์?
CSRF ํ ํฐ ์ ์ฅ์(CsrfTokenRepository) ๋น๊ต
CSRF ํ ํฐ์ ์์ฑ ๋ฐ ๊ด๋ฆฌ๋ CsrfTokenRepository๋ผ๋ ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๊ณ ๊ทธ๊ฒ์ ๊ตฌํํ ํด๋์ค์ ์์ํฉ๋๋ค.
- HttpSessionCsrfTokenRepository
- ๊ธฐ๋ณธ ์ค์
- CSRF ํ ํฐ์ HttpSession์ ์ ์ฅํฉ๋๋ค.
- Request ํค๋์ธ X-CSRF-TOKEN ๋๋ ์์ฒญ ๋งค๊ฐ๋ณ์์ธ _csrf์์ ํ ํฐ์ ์ฝ์ต๋๋ค.
- CookieCsrfTokenRepository
- CSRF ํ ํฐ์ XSRF-TOKEN ์ฟ ํค์ ์ ์ฅํฉ๋๋ค.
- ํ ํฐ์ XSRF-TOKEN ์ฟ ํค์ ์์ฑํฉ๋๋ค.
- Request ํค๋์ธ X-XSRF-TOKEN ํค๋ ๋๋ ์์ฒญ ๋งค๊ฐ๋ณ์์ธ _csrf์์ ํ ํฐ์ ์ฝ์ต๋๋ค.
- withHttpOnlyFalse() ์ค์ ์ผ๋ก Javascript์์ ์ฟ ํค๋ฅผ ์ฝ์ ์ ์๊ฒ ํ ์ ์์ต๋๋ค.
ํ ํฐ ์ ์ฅ์๋ฅผ ์ด๋ป๊ฒ ํ๋๋์ ๋ฐ๋ผ CSRF ํ ํฐ์ด ์ ์ฅ๋๋ ์์น๊ฐ ๋ค๋ฆ ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ ๊ฐ config ์ค์ ์์ ํ ํฐ ์ ์ฅ์๋ฅผ CookieCsrfTokenRepository๋ก ๋ช ์๋ฅผ ํ์ฃ .
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain chain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(auth -> auth
// ์ ์ ๋ฆฌ์์ค ์์ฒญ ํ์ฉ
.requestMatchers("/assets/**", "/favicon.ico", "/index.html").permitAll()
.requestMatchers("/", "/error/*").permitAll()
// Swagger ์์ฒญ ํ์ฉ
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html",
"/webjars/**").permitAll()
// Actuator ์์ฒญ ํ์ฉ
.requestMatchers("/actuator/**").permitAll()
// CSRF ํ ํฐ์ ๋ฐ๊ธํ๋ API ์์ฒญ ํ์ฉ
.requestMatchers("/api/auth/csrf-token").permitAll()
// ํ์๊ฐ์
API ์์ฒญ ํ์ฉ
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
// ๊ทธ ์ธ ์ธ์ฆ ํ์
.anyRequest().authenticated())
// ๐ ์ฌ๊ธฐ!! -----------------------------------------------------------------------
.csrf(csrf -> csrf
// JavaScript์์ ์ฟ ํค๋ฅผ ์ฝ์ ์ ์๊ฒ ํจ
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
// ---------------------------------------------------------------------------------
// LogoutFilter ์ ์ธ
.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.disable())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return httpSecurity.build();
}
๊ทธ๋ฌ๋ CsrfFilter์์๋ X-XSRF-TOKEN ํค๋์์ ๊ฐ์ ์ฝ๊ณ ์์์ต๋๋ค.
ํ๋ก ํธ์๋์์๋ ํ ํฐ์ ๋ฐ๊ธ๋ฐ์ X-CSRF-TOKEN ํค๋์ ์ ์ฅํ์ฌ ์์ฒญ์ ๋ณด๋ด๊ณ ์์์ผ๋
์๋ก ๋ง์ง ์์๋ ๊ฑฐ์ฃ .
์ฐธ๊ณ : https://bestdevelop-lab.tistory.com/100
โ ํด๊ฒฐ
ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๋ ๊ฐ์ง๊ฐ ์์ต๋๋ค.
1. ํ๋ก ํธ์์ CSRF ํ ํฐ์ X-XSRF-TOKEN ํค๋์ ๋ณด๋ด๊ฑฐ๋
2. CsrfFilter์์ X-CSRF-TOKEN ํค๋์์ ๊ฐ์ ์ฝ๊ฑฐ๋
2๋ฒ ๋ฐฉ๋ฒ์ ์์ฃผ ๊ฐ๋จํฉ๋๋ค.
SecurityConfig์์ csrf ์ค์ ์ CookieCsrfTokenRepository๋ฅผ ๋นผ๋ฉด ๋ฉ๋๋ค.
X-CSRF-TOKEN ํค๋์ ํ ํฐ์ ์ ์ฅํ๋ HttpSessionCsrfTokenRepository๋ ๊ธฐ๋ณธ๊ฐ์ด๋ ๋ฐ๋ก ๋ช ์ํ์ง ์์์ต๋๋ค.
๋ํ csrf ๋ณดํธ๋ ๊ธฐ๋ณธ์ด ํ์ฑํ์ด๋ฏ๋ก ๋ฐ๋ก ์ค์ ํ์ง ์์์ต๋๋ค.(disable()ํ๋ฉด ์ ๋จ)
์ด ๋ฐฉ๋ฒ์ผ๋ก ํ๋ 403 ์๋ต์ด ๋ ์ด์ ์ค์ง ์๊ณ ์ ์์ ์ผ๋ก ๋์ํฉ๋๋ค!
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain chain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(auth -> auth
// ์ ์ ๋ฆฌ์์ค ์์ฒญ ํ์ฉ
.requestMatchers("/assets/**", "/favicon.ico", "/index.html").permitAll()
.requestMatchers("/", "/error/*").permitAll()
// Swagger ์์ฒญ ํ์ฉ
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html",
"/webjars/**").permitAll()
// Actuator ์์ฒญ ํ์ฉ
.requestMatchers("/actuator/**").permitAll()
// CSRF ํ ํฐ์ ๋ฐ๊ธํ๋ API ์์ฒญ ํ์ฉ
.requestMatchers("/api/auth/csrf-token").permitAll()
// ํ์๊ฐ์
API ์์ฒญ ํ์ฉ
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
// ๊ทธ ์ธ ์ธ์ฆ ํ์
.anyRequest().authenticated())
// LogoutFilter ์ ์ธ
.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.disable())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return httpSecurity.build();
}
}