DevGang

[Spring Boot] CORS와 Preflight에 관한 삽질 본문

Study/Spring

[Spring Boot] CORS와 Preflight에 관한 삽질

별천랑 2021. 11. 24. 22:48

 처음으로 서버 사이드 랜더링이 아닌 클라이언트 사이드 랜더링 즉 프런트와 백엔드를 분리하여 룰루랄라 프로젝트를 진행하던 중 야생의 오류가 나타났다..! 바로 CORS와 Preflight⭐⭐⭐

 

첫번째 악당 CORS!

 클라이언트 측에서 보안상의 이유로 다른 출처, 즉 URL이 다른 리소스를 참고하는 것을 기본적으로 막고 있다.

따라서 클라이언트(http://lcoalhost:63342)에서 API 서버(http://localhost:8080)의 리소스를 참고하면 오류가 나온다.

 

 해당 오류를 해결하려면 API 서버 측에서 해당 클라이언트의 URL에 대해 리소스 참고를 허용하도록 헤더(Access-Control-Allow-Origin)에 값을 넣어서 응답 요청을 보내주어야 한다.

Access-Control-Allow-Origin 헤더에 값을 넣어 응답하는 그림

 

스프링에서는 해당 기능을 아래와 같이 간단하게 지원하고 있다.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:63342");
    }
}

 

하지만!!!

 

보스 등장!!!

이번 프로젝트에 사용자 인증/인가를 위해 Spring Security를 사용하고 있었는데 해당 오류를 만났다.

 

위와 같이 헤더에 Access-Control-Allow-Origin를 보내 해결할 수 있는 경우가 제한이 있었다.

  • 요청 메서드(method)는 GET, HEAD, POST 중 하나여야 한다.
  • Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안 됩니다.
  • Content-Type 헤더는 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나를 사용해야 합니다.

첫 번째 조건은 쉬운 조건이지만 2, 3번째 조건은 까다로운 조건이다. 나의 경우에는 2번 사항에 해당되는데 스프링 시큐리티를 사용하며 Authorization헤더를 사용하기 때문이다. 그리고 3번 조건은 많은 REST API들이 Content-Type으로 application/json를 사용하기 때문에 지키기 쉽지 않은 조건이다.

 

위에 3가지 조건을 만족하지 못하면 서버에 예비 요청(HTTP - OPTIONS)을 보내서 안전한지 판단한 후 본 요청을 보내는 Preflight 요청을 보낸다.

 

Preflight 요청 그림

프로젝트 스프링 시큐리티 설정

package shop.fevertime.backend.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import shop.fevertime.backend.security.jwt.JwtAuthenticationEntryPoint;
import shop.fevertime.backend.security.jwt.JwtAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAuthenticationFilter jwtRequestFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();

        http.authorizeRequests()
                .antMatchers(HttpMethod.GET, "/challenges/**").permitAll()
                .antMatchers(HttpMethod.GET, "/feeds/**").permitAll()
                .antMatchers("/login/kakao").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

위에 설정에서 Preflight 요청으로 보내는 모든 HttpMethod의 OPTIONS 메서드를 거부하기 때문에 생긴 오류였다.

그래서 아래와 같이 코드를 수정하니 정상 작동했다!!! 😂😂😂

package shop.fevertime.backend.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import shop.fevertime.backend.security.jwt.JwtAuthenticationEntryPoint;
import shop.fevertime.backend.security.jwt.JwtAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAuthenticationFilter jwtRequestFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();

        http.authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**/*").permitAll() // 해당 코드 추가
                .antMatchers(HttpMethod.GET, "/challenges/**").permitAll()
                .antMatchers(HttpMethod.GET, "/feeds/**").permitAll()
                .antMatchers("/login/kakao").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
참고 - https://beomy.github.io/tech/browser/cors/
Comments