๐ŸŒฟSpring

[Spring Security] CSRF ์„ค์ • 403 ์‘๋‹ต ๋ฌธ์ œ ํ•ด๊ฒฐ: CsrfTokenRepository์— ๋Œ€ํ•œ ์ดํ•ด

์†Œ์˜ ๐Ÿ€ 2025. 5. 20. 10:41

โš™๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

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();
    }
 }

 

 

๋ฐ˜์‘ํ˜•