ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • CORS(Cross-Origin Resource Sharing)
    WEB/etc 2014. 7. 4. 16:01
    Ajax에는 Same Origin Policy라는 원칙이 있다. 말 그대로, 현재 브라우져에 보여지고 있는 HTML을 내려준 웹서버(Origin)에게만 Ajax 요청을 보낼 수 있다.

    MS가 XMLHttpRequest를 처음 만들 때만 해도 이런 제약은 당연한 것처럼 보였지만, 지금에 와서는 OpenAPI를 통한 매시업(Mashup)이 활성화되는 데 가장 큰 장애물이 되었다. 매시업이 아니더라도 여러 개의 도메인을 사용해야 하는 대규모 사이트를 개발할 때도 골치거리였다. Same Origin Policy를 우회하는 방법으로 JSONP, IFRAME IO, CrossDomain Proxy 등이 고안되었지만, 보안성이 취약하다거나, 동기 호출이 안되거나, 주고 받는 데이터 형식이 제한되거나, 직관적이지 못하거나(dirty hack), ... 등의 문제점 때문에 표준화되기엔 무리가 있었다.

    (중략) 한 참 뒤에야 W3C는 (MS의 IE가 제공하는 방식을 수용하여) 크로스도메인간에도 Ajax요청을 주고 받을 수 있는 방법을 표준화 했는데, 그것이 바로 CORS다.

    CORS를 한 마디로 요약하면, "요청을 받은 웹서버가 허락하면 크로스도메인이라도 Ajax로 통신할 수 있다"라는 정책이다. 기술적으로는 크로스도메인에 위치한 웹서버가 응답에 적절한 Access-Control-Allow-류의 헤더를 보냄으로써 크로스도메인 Ajax를 허용 수 있다.

    말이 뺑뺑도는 느낌인데, 예를 들어 보자(코드를 줄이기 위해 jQuery를 사용했지만 XMLHttpRequest를 직접 사용해도 마찬가지다). Ajax 요청을 보내는 one.html을 내려 준 a.com이 오리진 웹서버다. 이 요청을 받는 b.com이 크로스도메인 웹서버다. a.com에서 b.com으로... 그래서 크로스-도메인이다.

    대충 그려보면 이런 식인데, b.com은 크로스도메인이므로 Ajax 통신이 불가능하지만, CORS를 적용하면 가능하다:

     

     



    http://a.com/one.html
    ...
    $.get('http://b.com/another.php', function(data) {
    alert(data);
    })

    http://b.com/another.php
    ...
    <?php header('Access-Control-Allow-Origin:*'); ?>
    ...


    보다시피, Ajax 클라이언트 측에는 추가적으로 할 일이 없고, 서버 측에서 할 일도 많지 않다. 웹서버(b.com)이 응답에 Access-Control-Allow-Origin헤더를 통해 모든 Origin(*)에서 오는 요청을 허락했으므로 다른 도메인(요청을 보낸 one.html을 내려 준 a.com)과 Ajax로 통신을 할 수 있다. 플래시에 익숙한 개발자라면 crossdomain.xml 을 떠오를 것이다. 맞다. 사실상 똑같다.

    CORS는 FF 3.5+, 사파리 4+, 크롬 등의 대부분의 현대적인 브라우져에서 지원한다. IE의 경우엔 IE8부터 지원하는데, XMLHttpRequest대신 XDomainRequest객체를 사용해야 한다. 즉, 거의 다 된다! IE6,7만 무시하면... oTL

    CORS 표준에 따르면 Origin외에도 HTTP 인증 여부, HTTP 메소드(GET, POST, ...), 특정 헤더 존재 유무 등의 다양한 기준으로 접근을 허용(preflight request)할 수 있는데, IE8은 이 스펙을 지원하지 않는다. -_-; CORS가 IE의 비표준 확장에 근거해서 만들어졌다는 점을 생각해보면.. 개그도 이런 개그가... -_-; IE9은... 아직 확인해보지 못했다. 

    클라이언트 대신 서버 측에서 뭔가를 해야 한다는 것은 장점인 동시에 단점인데, 기존의 수많은 OpenAPI들을 활용하고 싶어도 제공자들이 CORS를 적용하기 전까지는 무용지물... -_- OpenAPI를 제공하고 있거나, 제공할 계획이라면 JSONP 뿐 아니라 CORS도 고려해야 할 듯...

    처음 언급된지 십년이 다 된 "Web as a Platform"이 실감나는 요즘이다. 팀 버너스 리가 원하던 원치않던... 웹은 점점 플랫폼으로 진화(혹은 변신)하고 있다. 별것 아닌 CORS도 이런 관점에서 보면 조금은 다르게 보일지도...

     

    출처 : http://iolothebard.tistory.com/494 

     

    CORS 는 Cross-Origin Resource Sharing 의 앞글자를 따서 만들어진 단어이다. 한글로 직역하면 ‘근원이 다른 자원들을 공유하기’ 정도가 되겠다. 웹 세상에서 사용되는 언어인데, 조금 더 컴퓨터스럽게 풀어보자면 ‘다른 서버에서 제공하는 자원에 접근하기’ 가 적당할 것 같다. 하지만 여기서 origin 이란 꼭 물리적인 서버 장비를 말하는 것은 아니다. 서브도메인이 다르거나 포트가 다른 것도 다른 origin 으로 간주된다.

    CORS 는 Same-origin policy 를 우회할 수 있는 여러 방법중 대표적인 방법이다. 즉 브라우저의 Same-origin policy 를 합법적으로 피해서 다른 origin 에서 제공하는 자원에 접근하고 싶을 때 사용한다.

    CORS vs JSONP

    CORS 를 대신해서 Same-origin policy 를 우회하기 위해 일반적으로 사용하는 방법은 JSONP 이다. XHR 즉 자바스크립트를 이용해서 직접 요청을 하는 것이 아니라, script 태그를 DOM 에 삽입하여 브라우저가 script src 를 로드하도록 하는 방식이다. JSONP 방식은 자바스크립트로 요청하는 것이 아니므로, Same-origin policy 를 우회할 수 있긴 하다. 하지만 GET 메소드만 사용할 수 있다는 것과, 웹 보안을 위협한다는 점에서 되도록이면 사용하지 않는 것이 좋겠다. JSONP 의 웹 보안 위협에 대해 더 관심있는 분들은 CSRF (Cross-Site Request Forgery) 혹은 XSS (Cross-Site Scripting)에 대해 찾아보길 권한다.

    물론 일반적인 회사나 개발자라면 JSONP 방식을 사용해서 발생할 웹 보안 위협이 그닥 와닿지 않을 수 있다. 조금 더 정확히 말하면 해커들이 타겟으로 삼을만한 매력적인(?) 사이트들이나 신경써야 할 일이라고 치부될 가능성이 크다. 사실 많은 개발자들에게 (나도 예전엔 그랬고) JSONP 방식이 CORS 보다 더 익숙하다. 팀원들에게 당장 JSONP 사용을 중단하고 CORS 를 배워서 사용하자고 한다면 “그래! 훌륭한 생각이다. 우리 모두 다같이 웹보안에 더욱 신경쓰도록 하자!” 라고 말할 사람이 얼마나 있을까? 개발 일정 맞추기에 급급한 현실인데.

    CORS 와 일반 request 는 뭐가 다를까

    CORS 는 약속이다. 그럴듯하게 말하면 규약이라고도 한다. 그럼 도대체 누가 어떻게 하는 약속일까? 웹개발자라면 다들 알겠지만, 웹세상에서 힘 좀 쓴다는 분들이 모여서 약속을 정하는 곳이 있다. 국가로 치면 국회같은 입법기관인데, World Wide Web Consortium 줄여서 W3C 이다. W3C 의 규약 중 CORS 관련 규약이 존재한다. 이것을 각 브라우저 벤더들(크롬을 만드는 구글, IE를 만드는 마이크로소프트, 파이어폭스를 만드는 모질라 등등, 사파리를 만드는 애플 등등..)이 읽어보고 규약에 맞추어 브라우저를 만들게 된다. 이 벤더들은 국가로 치면 정부 즉 행정기관이다. 입법기관에서 만든 규약에 맞추어 그것을 실제 결과물로 만든다. 물론 현실에서도 그렇듯이 결과물이 규약과 100% 일치하지 않을 수도 있으며, 입법 과정에 영향력을 행사하기도 하고, 각 자치단체마다 규약을 해석하는 방식이 조금씩 다를 수 있다.

    CORS 는 일반 request 에 비해 추가적인 HTTP 헤더가 필요하도록 약속되어있다. CORS 에 필요한 헤더 목록을 정리해 보면 아래와 같다. 각 헤더의 자세한 역할은 위 W3C 의 CORS 관련 규약 문서를 읽어보기를 권한다.

    • Access-Control-Allow-Origin
    • Access-Control-Allow-Credentials
    • Access-Control-Allow-Methods
    • Access-Control-Allow-Headers
    • Access-Control-Max-Age
    • Origin
    • Access-Control-Request-Method
    • Access-Control-Request-Headers
    • 등등…

    Spring MVC 로 CORS 구현하기

    그럼 이제 Spring MVC 를 이용해서 CORS 를 구현하는 방법을 알아보자. 지금부터 나오는 모든 코드는내 깃허브의 Spring MVC 샘플 코드에 모두 포함되어있다.

    1. preflight 요청 처리하기

    preflight 요청이란 무엇일까? 단어 그대로이다. 실제 원하는 request 를 날리기 전에 먼저 날아가는 요청이다. 그렇다면 어느 상황에 preflight 요청이 수행되는 것일까? 모질라 개발자센터의 글을 참고해보자.

    “preflighted” requests first send an HTTP OPTIONS request header to the resource on the other domain, in order to determine whether the actual request is safe to send.

    실제 수행될 요청이 과연 “보내도 안전한 것인지” 판단이 필요할 때, preflight 요청이 날아가게 된다. CORS 요청의 경우 사용자 데이터가 다른 곳으로 전송될 소지가 있기 때문에 preflight 요청이 먼저 날아간다. 조금 더 자세하게 살펴보자.

    • It uses methods other than GET, HEAD or POST. Also, if POST is used to send request data with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, e.g. if the POST request sends an XML payload to the server using application/xml or text/xml, then the request is preflighted.
    • It sets custom headers in the request (e.g. the request uses a header such asX-PINGOTHER)

    request method 가 GET 이 아닌 경우, 그중에서도 특히 POST일 경우 Content-Type 에 따라서 preflight 요청이 수행될지 말지 결정된다. 또한 커스텀 헤더를 포함한 요청도 preflight 요청이 먼저 수행된다.

    자 그렇다면 이 preflight 요청을 Spring MVC 를 통해 어떻게 처리할지 알아보자. 아래 코드를 보자.

    1
    2
    3
    4
    ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", newDispatcherServlet(applicationContext));
    dispatcher.setLoadOnStartup(1);
    dispatcher.addMapping("/");
    dispatcher.setInitParameter("dispatchOptionsRequest", "true"); // CORS 를 위해서 option request 도 받아들인다.

    Spring 의 DispatcherServlet 은 기본적으로 OPTIONS 메소드를 무시하도록 만들어져있다. 왜 이렇게 만들어져있는지는 나도 잘 모르겠다. 아마 역사적인 이유가 있을 것 같은데.. 어찌 되었든 간에 그렇기 때문에 DispatcherServlet 을 띄울 때, OPTIONS 메소드 요청도 받아들이도록 설정해주어야 한다. dispatchOptionsRequest 를 true 로 세팅해주면 된다.

     
    /**
    * Preflight 요청을 처리하기 위한 컨트롤러.
    * @author mj
    *
    */ @Controller public class CorsController
    {
    private static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
    private static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
    private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
    private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
    private static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
    private static final Integer DAY_IN_SECONDS = 24 * 60 * 60;
    /**
    * Prefilght 요청을 처리한다.
    *
    * @param requestMethods
    * @param requestHeaders
    * @param response
    */
    @RequestMapping(method=RequestMethod.OPTIONS)
    public void handleOptionsRequest(@RequestHeader(value=ACCESS_CONTROL_REQUEST_METHOD, required=false) String requestMethods,
    @RequestHeader(value=ACCESS_CONTROL_REQUEST_HEADERS, required=false) String requestHeaders,
    HttpServletResponse response)
    {
    // response 헤더를 request 헤더와 동일하게 만든다. 제한이 필요하다면 필요한 값으로 설정한다. if (StringUtils.hasLength(requestMethods))
    {
    response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, requestMethods);
    }
    // response 헤더를 request 헤더와 동일하게 만든다. 제한이 필요하다면 필요한 값으로 설정한다. if (StringUtils.hasLength(requestHeaders))
    {
    response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
    }
    // 브라우저가 preflight 응답을 캐싱하도록 max age를 세팅해준다.response.setHeader(ACCESS_CONTROL_MAX_AGE, DAY_IN_SECONDS.toString());
    }
    }

    OPTIONS 메소드 요청을 모두 처리하는 컨트롤러를 하나 만들자. 이 컨트롤러가 가진 메소드는 브라우저가 CORS 요청을 하기 전에 보낸 preflight 요청을 처리해준다.

    내 코드는 전달받은 Access-Control-Allow-Methods 와 Access-Control-Allow-Headers 헤더의 내용을 그대로 다시 세팅해서 보내주도록 되어있는데, 제한이 필요하다면 원하는 값으로 세팅해주면 된다. 또한 브라우저가 preflight 요청을 캐싱하도록 Access-Control-Max-Age 값을 세팅해준다. 내 코드는 하루로 되어있는데 이 또한 원하는 값으로 해주면 된다.

    2. Cross-Origin 요청인 경우, 적절한 response header 붙이기

    Cross-Origin 요청이라는 것을 어떻게 판단할 수 있을까? 요청 프로토콜, 서브도메인, 호스트, 포트 등을 모두 뽑아내어 비교할 수도 있다. 근데 이건 너무 번잡스러운 것 같다.

    Cross-Origin 요청을 전송할 때, 브라우저는 Origin 이라는 헤더를 붙여서 보낸다. 이 때 서버에서는 Access-Control-Allow-Origin 이라는 헤더를 붙여서 보내주어야 하는데, 이 헤더가 존재하지 않거나 Origin 헤더의 값과 다를 경우, 브라우저는 접근할 수 없는 자원이라고 판단한다. 따라서 Access-Control-Allow-Origin 에 원하는 origin 값이나, wild card 즉 별표(*, asterisk)를 세팅해주어야 한다.

    아래 코드를 보자.

     
     
     
    @Configuration @EnableWebMvc @EnableAsync @ComponentScan(
    basePackages="com.nethru.test",
    excludeFilters=@ComponentScan.Filter(Configuration.class)
    ) public class MvcConfig extends WebMvcConfigurerAdapter // 인터셉터를 추가하기 위해 WebMvcConfigurerAdapter 를 상속한다 {
    @Bean
    public ViewResolver viewResolver()
    {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
    }
    /**
    * 인터셉터 추가
    */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
    registry.addInterceptor(new CorsInterceptor());
    }
    }

    인터셉터를 추가해주자. 이름은 CORS 를 처리하는 놈이므로 CorsInterceptor 로 결정했다.

     
    /**
    * CORS 를 위한 인터셉터.
    * Origin 헤더가 request 에 존재할 경우, CORS 가능하도록 응답 헤더를 추가한다.
    *
    * @author mj
    *
    */ public class CorsInterceptor implements HandlerInterceptor
    {
    private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
    private static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
    private static final String REQUEST_HEADER_ORIGIN = "Origin";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
    String origin = request.getHeader(REQUEST_HEADER_ORIGIN);
    // CORS 가능하도록 응답 헤더 추가 if (StringUtils.hasLength(origin))
    {
    // 요청한 도메인에 대해 CORS 를 허용한다. 제한이 필요하다면 필요한 값으로 설정한다.response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
    // credentials 허용 response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
    }
    return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception
    {
    // do nothing }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception
    {
    // do nothing }
    }

    위에서 설명한 그대로이다. Request Header 에 Origin 이 존재할 경우, Response Header 에 Access-Control-Allow-Origin 를 설정해준다. 내 코드에는 전달받은 Origin 을 다시 그대로 세팅해주었지만, 필요한 경우 원하는 값으로 설정하면 된다. 예를 들어 모든 Origin 을 허용하고 싶다면 Wild Card (*) 를 사용하면 된다.

     

     

    'WEB > etc' 카테고리의 다른 글

    ERR_CERT_COMMON_NAME_INVALID 오류  (0) 2019.04.03
    HTTP Method & Header  (0) 2014.10.10

    댓글

Designed by Tistory.