사진 출처: https://thehackerstuff.com/what-is-cors-a-detailed-comprehensive-analysis-of-cors/
CORS는 교차 출처 리소스 공유(Cross-Origin-Resource Sharing)의 약어로 한 도메인의 리소스를 다른 도메인과 상호 작용하는 것을 말한다.
그렇다면 CROS 오류가 발생하는 이유는 무엇일까?
그 이유는 기존의 정책이 동일 출처 정책(Same-Origin policy) 때문이다.
어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 보안방식이다. 이는 잠재적으로 해로울 수 있는 문서를 분리해서 공격받을 수 있는 경로를 줄이기 위해 사용된다.
따라서 웹페이지는 자신의 출처와 동일한 출처를 가지는 리소스에 대해서만 API 요청을 보낼 수 있고 이러한 정책을 우회하기 위해서는 JSONP(JSON with Padding) 이나 CORS같은 메커니즘을 사용하여 우회해야 한다.
URL은 위와 같은 구조로 이뤄져 있는데 이 때 포트번호가 명시된것이 아니라면 http는 80번 https는 443번 포트로 지정되어 생략되있다.
출처는 Protocol, Host, 포트번호를 의미하며 3개가 다 동일해야 같은 출처이다.
Cors의 동작 방식은 3개가 있다.
서버와 브라우저가 통신하기 위해 예비 요청을 보내고 확인 응답을 받은후 실제 요청을 받는 방식이다.
Preflight는 실제
이때 예비 요청은 OPTIONS
메서드로 예비요청을 보낸다.
이후 서버는 예비 요청에 대한 응답에 Access-Control-Allow-Origin
을 헤더에 포함하여 보내고 브라우저는 Access-Control-Allow-Origin
을 헤더에서 확인하여 CORS 동작 수행 여부를 판단한다.
예비 요청을 수행하는 조건은 다음과 같다.
Content-Type
이 Get
, Head
, Post
요청인증된 요청을 사용하는 방법으로 통신의 보안을 강화하고 싶을 때 사용 하는 방법이다.
클라이언트가 서버에 요청할 때 자격 인증 정보(Credential)을 담아서 요청하며 여기서 말하는 자격 인증 정보는 세션 ID가 저장되어 있는 쿠키나 Authorization 헤더에 설정하는 토큰 값 등을 의미한다.
프론트엔드와 백엔드 모드 해당 설정을 해야 하며 Credentail Request는 prefilght request가 선행되어야 한다.
Access-Control-Allow-Credentials
항목을 true로 설정 Access-Control-Allow-Origin
을 *
로 설정 금지Access-Control-Allow-Methods
를 *
로 설정 금지Access-Control-Allow-Headers
를 *
로 설정 금지특정 조건이 만족되면 Preflight Request이 생략하고 요청을 보내는 것이다.
특정 조건은 다음과 같다.
Get
, Head
요청Content-Type
헤더가 다음과 같은 POST
요청application/x-www-form-urlencoded
multipart/form-data
text/plain
Accept
, Accept-Language
, Content-Language
, Content-Type
, DPR
, Downlink
, Save-Data
, Viewport-Width
, Width
를 제외한 헤더를 사용하면 안된다.@CrossOrigin
을 원하는 메서드나 컨트롤러에 붙여주면 모든 도메인에 대해 접근이 허용된다.@CrossOrigin(origins = "http://domain.com)
과 같이 orgin 프로퍼티를 사용하면 특정 도메인만 허용하는 것도 가능하다.WebMvcConfigurer
를 추가한다.@SpringBootApplication
어노테이션이 붙어있는 파일에 WebMvcConfigurer
을 Bean으로 등록해줬다@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
.maxAge(1500)
}
};
}
}
addMapping
: 프로그램에서 제공하는 URLallowedOrigins
: 요청을 허용할 출처allowedHeaders
: 허용할 헤더allowedMethods
: 허용할 메서드allowCredentials
: 쿠키 허용에 대한 응답(true, false로 작성)maxAge
: prefilght 요청에 대한 응답을 브라우저에서 캐싱하는 시간public class CORSFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, origin, content-type, accept");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {}
public void destroy() {}
}
해당 Filter를 작성하고 등록해주면 된다.
이 때 주의할 점은 Authentication Filter 보다 앞단계에서 Filter가 진행되어야 한다.
response.header
: 응답하는 헤더 설정 Access-Control-Allow-Origin
: 요청을 허용할 출처Access-Control-Allow-Methods
: 허용할 메서드Access-Control-Allow-Headers
: 허용할 헤더 이름Access-Control-Max-Age
: 쿠키 허용에 대한 응답 @Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
configuration.addExposedHeader("Authorization");
configuration.addExposedHeader("Authorization-Refresh");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Spring Security를 사용하는 경우 SecurityConfig에서 Bean을 등록해주는 것으로 해결할수 있다.
CorsConfigurationSource
의 내부 로직
public void setAllowedOrigins(@Nullable List<String> origins) {
this.allowedOrigins = (origins == null ? null :
origins.stream().filter(Objects::nonNull).map(this::trimTrailingSlash).collect(Collectors.toList()));
}
public void addAllowedOrigin(@Nullable String origin) {
if (origin == null) {
return;
}
if (this.allowedOrigins == null) {
this.allowedOrigins = new ArrayList<>(4);
}
else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) {
setAllowedOrigins(DEFAULT_PERMIT_ALL);
}
origin = trimTrailingSlash(origin);
this.allowedOrigins.add(origin);
}
위와 같이 CorsConfigurationSource
의 set...
메서드는 List형식으로 값을 읽어오고 *
가 적용되지 않고
add...
식의 메서드는 *
가 적용되지만 한 개의 데이터만 입력받는다.