본문 바로가기

프로그래밍/Spring & MyBatis

[Spring] 크로스 도메인(CORS)

[Spring Framework]

다른 도메인에서 AJAX로 접근하면 Access-Control-Allow-Origin 에러가 발생한다.


해결 방법은 JSONP 요청.


JSONP로 ajax 호출을 하기 위해선 함수안에 json 문자열이 들어간 형식으로 서버에서 리턴 해줘야 한다.

ex) callback({"key":"value"});


1. 서버처리


 - callback 을 처리하기위해 JsonpAdvice 클래스 생성

   기존 json 처리와 동일하게 컨트롤러에서 처리가 가능하게 해준다.

1
2
3
4
5
6
@ControllerAdvice public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { public JsonpAdvice() { super("callback"); } @Override protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) { return MediaType.valueOf("application/javascript;charset=UTF-8"); } }

 - CORSFilter 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.io.IOException;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
/**
 *
 * @author ljo
 *
 */
public class CORSFilter implements Filter{
 
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
 
    }
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        ((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Origin", "*");
        ((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Methods","GET, OPTIONS, HEAD, PUT, POST");
        ((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept");
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        if (request.getMethod().equals("OPTIONS")) {
            resp.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        chain.doFilter(request, servletResponse);
    }
 
    @Override
    public void destroy() {
 
    }
 
}

 - 필터 등록

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
import com.solution.framework.filter.CORSFilter;
 
/**
 *
 * @author ljo
 *
 */
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Bean
    public FilterRegistrationBean getFilterRegistrationBean(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(new CORSFilter());
        registrationBean.addUrlPatterns("/test");
        registrationBean.addUrlPatterns("/test/*");
        return registrationBean;
    }
}

2. 클라이언트 요청

1
2
3
4
5
6
7
8
9
10
11
12
13
$.ajax({
    type : "post",
    url : "http://domain.com/test",
    contentType : "application/json",
    dataType : "json",
    async : true,
    success : function(data, textStatus, XMLHttpRequest) {
         
    },
    error : function(data, textStatus, errorThrown){
 
    }
});

jsonpCallback 은 굳이 붙여줄 필요는 없다.


-----------------------------------------------------------------------------------------------------------------------------------------


웹 개발시 JavaScript로 외부 서버에 ajax요청을 날리면 웹 브라우저의 콘솔창에 에러 메시지가 출력되면서 요청이 실패한다.
외부로 요청이 안되는 것은 JavaScript 엔진 표준 스팩에 동일 출처 정책이라는 보안 규칙이 있기 때문이다.

동일 출처 정책(Same-Origin Policy)
JavaScript(XMLHttpRequest)로 다른 웹 페이지에 접근할 때에 동일 출처의 페이지로만 접근이 가능하다. 
동일 출처라는 것은 프로토콜, 호스트명, 포트가 같은 것을 의미한다. 
즉 웹 페이지의 스크립트는 그 페이지와 같은 서버에 있는 주소로만 ajax 요청할 수 있다는 것이다.
이 정책이 초기에는 웹 사이트의 보안을 위한 좋은 방법으로 생각되었으나 최근들어 여러 도메인에 걸처서 구성되는 대규모 웹 프로젝트가 늘어나고, REST API 등을 이용한 외부 호출이 많은 상황에서는 까다로운 기술이기도 하다. 
그래서 CORS라는 정책이 만들어졌다. 특징으로는 서버에서 외부 요청을 허용할 경우 ajax요청이 가능해지는 방식이다.

CORS(Cross-Origin Resource Sharing)
CORS는 웹 페이지의 제한된 자원을 외부 도메인에서의 접근을 허용해주는 매커니즘이다.
브라우저와 서버간의 Cross-Origin 요청 허용여부를 안전하게 결정하도록 상호작용할 수 있는 방법을 정의한다.
요청 URL이 외부 도메인일 경우 웹 브라우저는 preflight 요청을 먼저 날린다. preflight 요청은 실제로 요청하려는 경로와 같은 URL에 대해 OPTIONS 메소드를 미리 날려 요청할 수 있는 권한이 있는지 확인한다. 서버로 넘어온 preflight  요청을 처리하여 웹 브라우저에서 실제 요청을 날릴 수 있도록 접근 허용 설정을 해줘야 한다. 권한이 없으면 에러를 발생시키고 권한이 있으면 실제 요청을 처리 해준다.

* Request headers (클라이언트의 요청 헤더)
Origin: 요청을 보내는 페이지의 도메인
Access-Control-Request-Method: 실제 요청하려는 메소드 종류
Access-Control-Request-Headers: 실제 요청에 포함되어 있는 헤더 이름

* Response headers (서버에서의 응답 헤더)
Access-Control-Allow-Origin: 요청을 허용하는 출처. * 인 경우, 모든 도메인의 요청을 허용한다.
Access-Control-Allow-Methods: 요청을 허용하는 메소드 종류. 헤더 값에 해당하는 메소드만 접근 허용한다. (default : GET, POST)
Access-Control-Allow-Headers: 요청을 허용하는 헤더 이름
Access-Control-Max-Age: 클라이언트에서 preflight 의 요청 결과를 저장할 시간(sec). 해당 시간동안은 preflight요청을 다시 하지 않게 된다.

위의 request header 값을 보고 response header에 해당 출처(origin)에 허용하는 요청 스펙을 허용해주도록 Filter나 Interceptor 등으로 구현해야 한다. 
아래와 같이 Spring Framework에서 제공하는 방법으로 구현할 수도 있다.

Controller method CORS configuration
@CrossOrigin 어노테이션을 사용하여 클래스/메소드 레벨로 정의하여 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
 
    @CrossOrigin("http://domain.com")
    @RequestMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }
 
    @RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
cs

Global CORS configuration - Java Config
Spirng MVC 기반의 전역 설정으로 Filter와 흡사하고 @CrossOrigin과 함께 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
             .allowedOrigins("http://domain.com")
             .allowedMethods("PUT""DELETE")
             .allowedHeaders("header1""header2""header3")
             .allowCredentials(false).maxAge(3600);
    }
}
cs

Filter based CORS support
Spring Framework 에서 지원하는 CorsFilter를 사용하거나 Custom하여 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
 
public class MyCorsFilter extends CorsFilter {
 
    public MyCorsFilter() {
        super(configurationSource());
    }
 
    private static UrlBasedCorsConfigurationSource configurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://domain.com");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}
cs

출처 - http://adrenal.tistory.com/16

출처: http://mirotic91.tistory.com/category/Development/Security [Turning Point]