[SpringSecurity] CORS - 실전편 : 상세한 설정 가이드

유알·2023년 5월 30일
1

[SpringSecurity]

목록 보기
12/15

이전글 : [SpringSecurity] CORS - 이론편 : 항상 걸리적 거리는 녀석

글의 개요

사실 이전의 이론편만 적고 이 CORS의 고통에서는 영원히 해방된줄 알았으나, 그때 쯤 프론트에서 연락이 왔다.

cors 설정 좀 고쳐주세요
https://bogmong.tistory.com/5

간단히 요약하자면, React에서 Axios라이브러리를 통해 POST요청을 보내는데, JWT 토큰이 담겨있는 Authorization 헤더가 안받아진다는 것이다. PostMan에서는 잘 받아지는데 말이다.

딱 이 말을 듣자마자 용의자가 누군지 알 수 있었다. 브라우저다. 분명히 응답을 잘 받았는데, CORS 설정 때문에 필터링했구나 이 녀석 😤

그래서 이번편을 작성하게 되었다. 분명히 CORS는 나의 개발의 삶과 함께 꾸준히 고통받을 녀석인것 같기에, 추후 참고할 만큼 직관적이고, 핵심 코드만 있는 글을 쓰기로 하였다.

Spring Security의 CORS 설정

Spring Security의 설정에는 두가지 방식이 있다.

  • WebSecurityConfigurerAdapter를 상속
  • @Configuration 과 @Bean을 통해 설정

첫번째 설정은 Spring Security 5.7.0-M2 부터 deprecated되어 버렸다.
따라서 두번째 방법으로 작성한다.

CorsFilter 활성화

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity security, AccessToken.TokenBuilder tokenBuilder) throws Exception {

        // disables
        return security
                .cors()
                .and()
                .build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
    	//설정들
        return source;
    }

}

자 복붙하지말고 공부해보자.

  • securityFilterChain 에서 cors를 활성화 시켜주면, CorsFilter가 활성화 된다. (필터체인에 등록된다.)
  • CorsFilter에는 CorsConfigurationSource가 주입된다. 따라서 빈으로 등록해주자

CorsConfigurationSource 등록

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(List.of("*"));
        configuration.addAllowedMethod("*");
        configuration.addAllowedHeader("*");
        configuration.setExposedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

사실 SpringSecurity가 너무 좋은게 이 간단한 설정만으로 Cors 기능을 제공해준다.
대신 이것 조차 공부하기 싫어서 구글에서 복붙해오면, 말그대로 주먹구구식 코딩이 되는 것이다.(나를 말하는 것이다.)

자 CorsConfigurationSource가 무엇인지 보자. 딱 하나의 메서드만 있는 함수형 인터페이스다.
request 에 따라 적절한 CorsConfiguration을 리턴해준다.

기본적으로 많이 쓰는 UrlCorsConfiguration을 보자.

public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {

	//...
    
	private final Map<PathPattern, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
    
    //...
    
    @Override
	@Nullable
	public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
		Object path = resolvePath(request);
		boolean isPathContainer = (path instanceof PathContainer);
		for (Map.Entry<PathPattern, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
			if (match(path, isPathContainer, entry.getKey())) {
				return entry.getValue();
			}
		}

		return null;
	}

핵심 부분만 가져왔다. 간단하게 말해서, Map에 PathPattern과 CorsConfiguration을 등록해 놓고, 요청에 적절한 CorsConfiguration을 넘겨주는 것이다.

따라서 구지 관습적으로 UrlCorsConfiguration 를 복붙 할게 아니라, 본인만의 CorsConfiguration을 구현해도 충분하다.

CorsConfiguration 설정 📌

여기가 사실상 핵심 부분이다. 프레임워크가 좋은점이 무엇인가? 설정만 해주면 알아서 돌아간다는 것이다.
설정을 가장 실용적이게 알아보자.

필드와 상수들이다. 보면 이 설정들을 가져다 쓴다고 이해할 수 있다.

origin 관련 설정

	@Nullable
	private List<String> allowedOrigins;

	@Nullable
	private List<OriginPattern> allowedOriginPatterns;

둘은 같아보이는데 무슨 차이가 있을까?

  • 위에 것은 말 그대로 가능한 Origin을 하나씩 등록한다.
  • 아래는 regex 패턴에 기반한 Url 매칭을 지원해준다. (내부클래스로 정의되어 있다.)
  • 👉 그리고 가장 중요한 것!! allowCredentials 가 true일 때는 절대 String기반으로 "*"을 줄 수 없다. 이유는 이전편 참고
  • 따라서 allowCredential로 credential cors를 허용할 때는 List.of("*")으로 우회해 사용할 수 있다. ->하지만 절대 권장하지 않는다. 절대

그래서 어떻게 설정을 할까?
복붙 블로그 들에는 전부 허용하는 설정만 복사해서 사용하고 있다. 실제상황에서는 어떻게 설정해야할까?

  • 우리의 서버로 요청 보내기를 허용하는 도메인을 지정해주어야한다.
  • 예를 들어 우리의 프론트를 보여주는 서버가 http://www.front.comm:8080808 로 되어 있다면, 이 도메인을 등록해주어야 한다.
  • 즉 우리의 서버는 이쪽 도메인에서 우리 서버(크로스 도메인)으로 보내는 리소스 요청과 응답을 허용해 주어야 한다.

메서드 설정

브라우저는 CORS를 체크할 때 메서드도 체크한다. 무슨 말일까?

브라우저 입장 : 우리 도메인에서 너네 다른 도메인에 이러한 메서드로 리소스 보낼건데, 가능?
우리 서버 : ㅇㅇ 쌉가능

그러니까 다른 도메인에서 우리 서버로 GET, POST 처럼 메서드를 보내는게 허용되냐고 묻는것이다. (너무 깐깐하다)

	@Nullable
	private List<String> allowedMethods;

	@Nullable
	private List<HttpMethod> resolvedMethods = DEFAULT_METHODS;
    
    //참고
	private static final List<HttpMethod> DEFAULT_METHODS = Collections.unmodifiableList(
			Arrays.asList(HttpMethod.GET, HttpMethod.HEAD));

메서드를 설정하는 것도 두가지 옵션이 있다. 구지 큰 차이는 없지만 둘다 허용할 메서드를 지정하라고 되어 있다.

자 복붙 블로그와의 차이를 위해 실전에서 어떻게 설정해야할까?

  • 모든 메서드를 허용하는 것이 아니라, 다른 도메인에서 요청가능한 메서드를 지정해주면 된다.
  • 예를 들어 다른 도메인에서는 GET만 가능하도록 할 수도 있다.
    하지만 헷갈리면 안되는게, 이미 오리진이 체크된 도메인들을 대상으로 메서드를 필터링하는 것이므로, 이를 고려해서 설정해야한다.
  • 사실, 이게 Restful api 도 그렇고, 이것도 그렇고 나의 개인적인 생각은, 기존의 정적 리소스를 제공해주는 전통적인 웹사이트에 맞춰진 설정이라는 생각이 지워지지 않는다. 요즘같이 json을 기반으로한 api 서버가 있고, 프론트가 분리된 서버에서 이게 어느정도의 의미가 있을까 싶다.(개인적인 생각)

헤더 설정

여기가 좀 까다롭다. 이번 글을 쓰게된 이유도 이 의미를 명확히 파악하지 않아서 쓰게된 것이다.

	@Nullable
	private List<String> allowedHeaders;

	@Nullable
	private List<String> exposedHeaders;

둘의 차이를 알겠는가?

  • Access-Control-Expose-Headers 는 클라이언트(브라우저기반)가 응답에서 접근할 수 있는 헤더이다.
    이게 설정안되어 있으면 클라이언트에서 이런 문의사항이 올 것이다.
    Postman에서는 이 헤더가 읽히는데 axios로 받아보면 헤더가 없어요 🙀
  • Access-Control-Allow-Headers 는 Access-Control-Allow-Headers 는 preflight request의 응답에 사용되는 헤더이다.
    실제 요청(본 요청)에 사용할 수 있는 HTTP 헤더의 목록을 나열합니다.

그러니까 완전 다르다. allowedHeaders 는 클라이언트가 본 요청(이거 모르면 이전 글 참고)에서 사용할 수 있는 헤더 목록을 나타낸다.
exposedHeaders 는 클라이언트가 응답(리소스)을 받은 후 읽기(접근)이 허용되는 헤더를 말한다.

credentials 와 maxAge 설정

	@Nullable
	private Boolean allowCredentials;

	@Nullable
	private Long maxAge;

credentials에 대해서는 나도 다소 헷갈리는 부분이다. 하지만 내가 이해한 대로 설명하자면, 쿠키와 같은 보안요청을 본 요청에 포함해도 되는지 말해주는 헤더로 이해하고 있다.
대신 이렇게 설정하면 보안이 좀더 빡빡해지는데, 대표적으로 allow origin을 와일드카드(*)로 설정하지 못한다. (자세한 내용은 이전글 참조)
애초에 이 값을 true로 주고, addAllowedOrigin("*")과 같이 설정했다면, 내부 로직 중 예외가 발생한다.

(나라면 application 실행시 체크해서 예외를 던져서 실행 자체가 안되게 막을텐데, 런타임 중에 체크한다. 나중에 spring security 에 pr을 보내봐야겠다.)

maxAge의 경우, preflight의 응답을 클라이언트가 캐싱할 수 있는 시간을 지정한다. 따라서 클라이언트가 이 preflight 요청을 자주 갱신해서 cors 세팅을 업데이트하기를 바란다면, 이 값을 짧게 설정하면 될 것이다.

profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글