CORS 트러블 슈팅

박진선·2023년 6월 15일
1
post-custom-banner

문제상황

CorsConfigurationSource 를 Bean 으로 등록하여 Cross-Origin Request 에서 발생하는 Cors 이슈들을 해결하려고 했으나 Bean으로 등록되지 않거나, 헤더 설정 오류 이슈들의 해결과정을 기록하려고 한다.

기존 코드

@Configuration
@RequiredArgsConstructor
public class SecurityConfiguration {
  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ...
    ...
    http.cors();
    return http.build();
    
  }
  
  @Bean
  CorsConfigurationSource corsConfiguration() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowedOrigins(List.of("*"));
    corsConfiguration.setAllowedMethods(List.of("*"));
    corsConfiguration.setAllowedHeaders(List.of("*"));
    corsConfiguration.setMaxAge(3000L);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    return source;

    return source;


}

원인파악

클라이언트에서 서버에게 자격 인증 정보(Credential)를 헤더에 담아 전송하려면 인증된 요청 (Credentialed Request) 에 맞는 준수 사항을 지켜야 한다는 것을 알게 됐다.
프로젝트 에서는 JWT를 Authorization 헤더에 담아 인증(authentication), 인가(authorization) 정보를 주고 받기 때문에 인증된 요청을 사용해야 한다.
서버에서 해주어야할 작업은 아래의 각 헤더를 설정하여 Preflight Request 가 올 경우 반환 해주어야 한다.

  1. Access-Control-Allow-Credentials: true 로 설정한다. true가 아닐 경우 클라이언트에서 ajax나 FetchAPI 등 API 요청 메소드를 보낼때 withCredentials 옵션을 true 로 설정하여 요청을 보내도 서버에서 해당 헤더가 true가 아닐 경우 브라우저에서 Reponse 를 무시해 버린다.

  2. Access-Control-Allow-Origin 은 와일드카드를 사용하는 것이 아닌 특정 Origin을 명시해야 한다. 와일드카드를 사용할 경우 Cors 에러가 발생한다.
    헤더에는 단일 Origin만 지정하여야 한다. 서버에서 여러가지 Origin을 허용하더라도 요청 Origin에 따라 해당 Origin을 허용하는 경우 해당 Origin 만 Access-Control-Allow-Origin 헤더에 지정해야 한다. 해당 기능을 하는 로직은 이미 CorsFilter에 구현이 되어있다.

  3. Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Expose-Headers 의 경우 와일드카드 * 를 설정하면 * 이라는 이름을 갖는 특별한 의미가 없는 값으로 취급된다.

헤더 설정 정보를 기반으로 코드를 다시 아래와 같이 수정하였다.

@Configuration
@RequiredArgsConstructor
public class SecurityConfiguration {
  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ...
    ...
    http.configurationSource(corsConfigurationSource()); // corsConfigurationSource 를 등록하는 또다른 방법
    return http.build();
    
  }
  
  @Bean
  CorsConfigurationSource corsConfigurationSource() { // corsConfiguration -> corsConfigurationSource 변경
    CorsConfiguration corsConfiguration = new CorsConfiguration();

    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.setAllowedOrigins(Arrays.asList("http://chamongbucket.s3-website.ap-northeast-2.amazonaws.com/","http://localhost:3000/"));
    corsConfiguration.setAllowedHeaders(Arrays.asList("Authorization", "Refresh"));
    corsConfiguration.setExposedHeaders(Arrays.asList("Authorization", "Refresh"));
    corsConfiguration.setAllowedMethods(Arrays.asList(
      HttpMethod.POST.name(), HttpMethod.PATCH.name(), HttpMethod.GET.name(), HttpMethod.DELETE.name(), HttpMethod.OPTIONS.name()));
    corsConfiguration.setMaxAge(3600l);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);

    return source;
    
  }
}

하지만 Bean으로 등록되지 않는 이슈가 해결되지 않아 디버깅을 통해 원인을 찾아낼 수 있었다. 아래 로직은 CorsConfigurer 클래스의 코드를 일부 발췌한 것으로 CorsFilter를 등록하는 로직이다. getCorsFilter 메소드 내부를 보면 context.containsBean() 메소드를 호출 해 corsConfigurationSource 라는 이름의 Bean이 포함되어 있는지를 확인 후 존재할 경우 해당 Bean을 가져온 뒤 CorsFilter 인스턴스를 생성하여 생성자 인자값으로 전달하여 반환하는 로직을 볼 수 있다. 즉 corsConfigurationSource 이름의 Bean 이 생성되지 않았던 것이 원인 이었다. Bean 은 메소드명과 똑같이 설정되기 때문에 메소드명을 변경 후 정상적으로 등록 됨을 확인 하였다. 다른 방법으로 Bean 으로 등록하지 않고 해결할 수 있는 방법이 있는데http.configurationSource(corsConfigurationSource()); 이와 같이 설정하면 CorsConfigurationSource 를 CorsConfigurer 클래스의 configurationSource 필드에 저장되어 getCorsFilter 메소드 내부 첫번째 로직에서 configurationSource 필드가 null 이 아닐 경우 CorsFilter 인스턴스를 생성하여 생성자 인자값으로 전달하여 반환한다.

public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<CorsConfigurer<H>, H> {
	private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";

	private static final String CORS_CONFIGURATION_SOURCE_BEAN_NAME = "corsConfigurationSource";

	private static final String CORS_FILTER_BEAN_NAME = "corsFilter";

	private CorsConfigurationSource configurationSource;
    
	...
    ...
    
    @Override
	public void configure(H http) {
		ApplicationContext context = http.getSharedObject(ApplicationContext.class);
		CorsFilter corsFilter = getCorsFilter(context);
		Assert.state(corsFilter != null, () -> "Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
				+ CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
		http.addFilter(corsFilter);
	}
    
    private CorsFilter getCorsFilter(ApplicationContext context) {
		if (this.configurationSource != null) {
			return new CorsFilter(this.configurationSource);
		}
		boolean containsCorsFilter = context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
		if (containsCorsFilter) {
			return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
		}
		boolean containsCorsSource = context.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
		if (containsCorsSource) {
			CorsConfigurationSource configurationSource = context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME,
					CorsConfigurationSource.class);
			return new CorsFilter(configurationSource);
		}
		boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, context.getClassLoader());
		if (mvcPresent) {
			return MvcCorsFilter.getMvcCorsFilter(context);
		}
		return null;
	}


}

최종적으로 코드를 수정 후 Cors 에러를 해결할 수 있었다.

profile
주니어 개발자 입니다
post-custom-banner

0개의 댓글