이러쿵저러쿵


웹 사이트 개발 시, 주요한 이슈중의 하나를 꼽자면 크로스 도메인(Cross Domain)이 있습니다.

 

최근 대부분의 웹 브라우저는 Javascript(JQuery)를 이용하여 AJAX 등을 통해서 다른 도메인의 서버의 URL 을 호출하여 데이터를 가져오는 경우, 보안 문제를 발생시킵니다.

 

만약 우리 웹 서비스에서만 사용하기 위해 다른 서브 도메인을 가진 API 함수를 제공하는 API 서버를 구축하였는데, 다른 웹 서비스에서 이 API 서버에 접근하여 마음대로 API를 호출하여 사용한다면 문제가 되겠죠.

 

그래서 Javascript 는 동일 출처 정책(Same Origin Policy) 라는 정책을 두어 다른 도메인의 서버에 요청하는 것을 보안 문제로 간주하고 이를 차단합니다. 즉, Javascript는 자신이 속한 동일한 도메인 내에서만 서버 요청을 허용하고, 처리해주겠다는 것인데요. 이것은 www.ozit.co.kr 도메인에서 호출된 AJAX는 www.ozit.co.kr 도메인 내에 있는 URL만을 호출할 수 있다는 의미입니다. 다시말하면 www.ozit.co.kr 도메인에서 www.tistory.com 의 URL을 AJAX로 호출할 수 없다는 의미이죠.

 

이를 다른 말로는 샌드박스(Sandbox)라고도 합니다. 샌드박스는 보호된 영역 안에서만 프로그램을 동작시킬 수 있도록 하며, 외부에 의해 영향을 받지 않도록 하는 모델을 말하는데요. 이 말뜻은 단어에서 유추할 수 있듯이 어린아이들이 뛰어놀 때, 다치지 않고, 그 안에서만 놀 수 있도록 만든 '모래 놀이통'에서 왔습니다.

 

그런데, 하나의 도메인을 가진 웹 서버에서 모든 처리를 하기에는 효율성이나 성능 등 여러 문제로 각 기능별도 여러 서버를 두는 경우가 많은데요. (API 서버, WAS 서버, 파일(이미지) 서버 등등)

물리적으로 분리된 서버이고, 다른 용도로 구축된 서버이니, 당연히 각각 다른 도메인을 가진 서버들일 텐데, 서로간에 AJAX 통신을 할 수 없는 것일까요? 즉, 서로 다른 도메인간의 호출을 의미하는 크로스 도메인 문제를 해결할 수는 없는 것일까요?

(물리적으로 동일한 서버에서도 여러 도메인을 사용할 수 있는데, 이 때에도 동일하게 크로스 도메인 이슈는 발생합니다. 크로스 도메인은 물리적인 서버나 환경의 이슈가 아닌 도메인 이름 자체의 이슈입니다.)

 

참고로 아래에서 설명드릴 방식은 서버측에서 크로스 도메인을 허용하여 문제를 해결하는 방식인데, 서버의 수정 없이 클라이언트 단에서 이를 해결하는 방법 또한 있습니다. 하지만 100% 해결은 불가능합니다. jsonp, document.domain 값 설정, 크롬 브라우저의 특수 옵션 사용 등 몇몇 방법이 있으나, 범용적인 웹 서비스에서 사용이 어렵거나(사용자가 직접 세팅해야 한다거나), 그 기능이 제한적인 경우가 대부분입니다. 실제 서버에서 해결해주는 것이 표준화된 방법이고, 100% 해결 가능한 방법입니다.

 

 

아래는 크로스 도메인 문제가 발생하면 뜨는 오류 메시지입니다.

 

 

XMLHttpRequest cannot load http://www.ozit.co.kr.
No 'Access-Control-Allow-Origin' header is present on the requested resource
.
Origin 'http://abc.ozit.co.kr' is therefore not allowed access.

 

 

 

 

 

 

AJAX는 자원 요청시 XMLHttpRequest 를 통해서 처리되기 때문에 위와 같은 오류가 표시되었습니다.

 

 http://abc.ozit.co.kr 도메인의 웹 서비스에서 http://www.ozit.co.kr 도메인으로 AJAX 호출을 하였으나, 접근할 수 없다는 말입니다.

XMLHttpRequest는 http://www.ozit.co.kr 을 불러올 수 없는데, 그 이유가 Access-Control-Allow-Origin 헤더가 요청된 자원(http://www.ozit.co.kr)에 존재하지 않기 때문이라는데요.

 

파이어폭스로 보는 경우, 좀 더 자세한 한글로 된 안내를 보실 수 있는데요.

"교차 원본 요청 차단: 동일 출처 정책으로 인해 http://www.ozit.co.kr 에 있는 원격 자원을 읽을 수 없습니다. 자원을 같은 도메인으로 이동시키거나 CORS를 활성화하여 해결할 수 있습니다." 라고 합니다.

 

같은 도메인을 사용한다면 당연히 문제가 해결되겠지만, 그러지 못하는 경우라면 CORS를 활성화 시키면 되겠군요.

CORS를 간단히 설명드리자면 Cross-Origin Resource Sharing 의 약자이며, 웹 페이지의 제한된 자원을 외부의 도메인에서의 요청(접근)을 허용해주는 매커니즘입니다. CORS는 브라우저와 서버간의 Cross-Origin 요청을 허용할지 안할지에 대한 여부를 안전하게 결정하도록 상호작용할 수 있는 방법을 정의합니다.

 

 

참고 : CORS (Cross-origin resource sharing)

http://en.wikipedia.org/wiki/Cross-origin_resource_sharing

 

 

설명이 이것저것 잡다하게 많은데요, 실제 이를  해결하는 방법은 어렵지 않습니다.

크로스 도메인 요청을 허용할 웹 서버에서 크로스 도메인 이슈를 문제 삼지 않을 도메인을 지정해주면 됩니다.

 

들어오는 모든 요청에 대해 처리해주기 위해 Filter 를 하나 만들어야 될 것 같습니다.

필터(Filter) 이름은 SimpleCORSFilter 로 하기로 하고 SimpleCORSFilter class 를 Filter interface를 구현하여, 생성하도록 합니다.

그리고 이 필터를 웹서비스의 web.xml 에 등록하여 줍니다.

 

 

- web.xml -

<filter>
    <filter-name>cors</filter-name>
    <filter-class>com.company.project.util.domain.SimpleCORSFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>cors</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

 

 

 필터 이름은 cors이며, com.company.project.util.domain 패키지 내에 SimpleCORSFilter 클래스를 구현하였습니다.

 모든 URL 패턴(/*)에 대해 필터가 적용되도록 하였습니다.

 

 

 

- SimpleCORSFilter.java -

 

package com.company.project.util.domain;

 

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.HttpServletResponse;

import org.springframework.stereotype.Component;

 

@Component
public class SimpleCORSFilter implements Filter {
 
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletResponse response = (HttpServletResponse) res;


        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
        
        response.setHeader("Access-Control-Allow-Origin", "*");

     


        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {}

    public void destroy() {}

}

 

위의 필터는 web.xml 에 정의한 URL 패턴에 의해서 이 웹 서버로 오는 모든 요청이 이 메서드를 지나치게 됩니다. 이 메서드를 한 번 지나고 나면 결과를 내려받는 HTML 페이지 Header에 Access-Control 과 관련된 4개의 라인이 추가되게 됩니다.

 

* response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");

 - POST, GET, OPTIONS, DELETE 요청에 대해 허용하겠다는 의미입니다.

 

* response.setHeader("Access-Control-Max-Age", "3600");
 - HTTP Request 요청에 앞서 Preflight Request 라는 요청이 발생되는데, 이는 해당 서버에 요청하는 메서드가 실행 가능한지(권한이 있는지) 확인을 위한 요청입니다. Preflight Request는 OPTIONS 메서드를 통해 서버에 전달됩니다. (위의 Methods 설정에서 OPTIONS 를 허용해 주었습니다.)

 여기서 Access-Control-Max-Age 는 Preflight request를 캐시할 시간입니다. 단위는 초단위이며, 3,600초는 1시간입니다. Preflight Request를 웹브라우저에 캐시한다면 최소 1시간동안에는 서버에 재 요청하지 않을 것입니다.

 

* response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
 이는 표준화된 규약은 아니지만, 보통 AJAX 호출이라는 것을 의미하기 위해 비공식적으로 사용되는 절차입니다. JQuery 또한 AJAX 요청 시, 이 헤더(x-requested-with)를 포함하는 것을 확인하실 수 있습니다. 여기서는 이 요청이 Ajax 요청임을 알려주기 위해 Header 에 x-request-width를 설정합니다. Form을 통한 요청과 Ajax 요청을 구분하기 위해 사용된 비표준 규약지만, 많은 라이브러리에서 이를 채택하여 사용하고 있습니다. (참고로 HTML5 부터는 Form 과 Ajax 요청을 구분할 수 있는 Header가 추가되었습니다.)

 

* response.setHeader("Access-Control-Allow-Origin", "*");
 이 부분이 가장 중요한 부분입니다. * 는 모든 도메인에 대해 허용하겠다는 의미입니다. 즉 어떤 웹사이트라도 이 서버에 접근하여 AJAX 요청하여 결과를 가져갈 수 있도록 허용하겠다는 의미입니다.

 만약 보안 이슈가 있어서 특정 도메인만 허용해야 한다면 * 대신 특정 도메인만을 지정할 수 있습니다.

 

 

response.addHeader("Access-Control-Allow-Origin", "*");

 

대신 

 

response.addHeader("Access-Control-Allow-Origin", "http://www.ozit.co.kr");
response.addHeader("Access-Control-Allow-Origin", "http://abc.ozit.co.kr");
response.addHeader("Access-Control-Allow-Origin", "http://test.ozrank.co.kr");

 

 

이렇게 쓰면

 

www.ozit.co.kr, abc.ozit.co.kr, test.ozrank.co.kr 이렇게 3개의 도메인에 대해서만 크로스 도메인을 허용하겠다는 의미입니다.

 

 

저작자 표시 비영리 변경 금지
신고


Comment +20

  • 2015.06.18 16:54

    비밀댓글입니다

    • 안녕하세요. 유상훈님. 제 블로그를 방문해 주셔서 감사합니다.

      코드 일부분만 볼 수 있어서 정확한 답변은 드리기 어려우나, 에러로 보아서는 Cross Domain 이슈로 보입니다.

      먼저 서버 쪽에 response.setHeader("Access-Control-Allow-Origin", "*"); 코드를 포함한(또는 열어줄 도메인에 대해 등록) CORS 필터를 등록하신 후에(포스팅 내용 참조)
      다른 도메인 간의 데이터 전송 및 처리 요청이라면 beforSend:function(xhr) 메서드 바로 위쪽에 crossDomain: true 세팅값을 한 번 포함시켜 보시기 바랍니다.

      $.ajaxSetup({
      crossDomain: true,
      beforeSend: function(xhr) {
      xhr.setRequestHeader("Application-Client-ID", "hue);
      xhr.setRequestHeader("Application-Uniqid", "12415322");
      }
      });

      또는 아래 처럼..
      $.ajaxSetup({
      beforeSend: function(xhr, settings) {
      if (this.crossDomain) {
      xhr.setRequestHeader("Application-Client-ID", "hue);
      xhr.setRequestHeader("Application-Uniqid", "12415322");
      }
      }
      });


      아니면 ajaxSetup 의 속성값 중에 dataType 을 jsonp (jsonp 콜백함수도 지정 가능)로 하시고, 서버쪽에서도 jsonp 형태로 구현해 주시면 되지 않을까 싶습니다.

      실제 유사한 케이스의 코드를 작성돌려 본 코드는 아니라, 작동 여부는 확인하지 못하였습니다. 도움이 되었으면 합니다.

      좋은 밤 되세요~

  • 노도 2015.12.03 15:59 신고

    감사합니다 덕분에 빠르게 해결했어요~

    • 안녕하세요. 노도님!

      문제가 해결되셨군요!!! 야호~~ ㅎㅎㅎ
      도움이 되어 뿌듯합니다. ㅎㅎㅎ

      즐거운 주말 보내세요!

  • 지나가는개발자 302 2015.12.24 10:01 신고

    안녕하세요...

    이문제때문에 엄청 힘들었는데 감사합니다. 깔끔한 설명과 정리 감사합니다.

    다만 Access-Control-Allow-Origin 해더값 정의하는 부분에서 다중으로 도메인을 허용하는 부분이

    적용되지 않더라구요... spring 프레임 워크 에서는 가능한지는 확인하지 못하였지만

    1개의 도메인만 허용한다는 문구가 나오는 문제가 있었습니다. 그렇다고 *로 하기는 왠지 찝찝하고...

    혹시나 저와 같은 문제가 있으신 분은 https://www.owasp.org/index.php/CORS_OriginHeaderScrutiny 이 내용을

    참고하시면 될 것 같습니다ㅎ.

    어쨌든 좋은 포스팅 감사합니다

    • 안녕하세요. 지나가는개발자 302님.

      블로그를 방문해 주셔서 감사합니다.
      좋은 말씀 감사드리고, 좋은 정보도 제공해 주셔서 감사드립니다.
      저도 참고해 보아야 할 것 같습니다.^^

      즐거운 연휴 보내세요!

  • 도움을 주세요ㅜㅜ 2016.03.15 14:10 신고

    이 문제때문에 고생하고 있는 한명입니다...ㅜㅜ
    다른 웹페이지를 불러와야 하는 상황인데 iframe 태그에서 사용중인데...ajax로 url을 확인한다음 iframe에 src에 url을 넣어주려고 하는데...ajax에서 확인 시 계속 위에서 동일한 에러가 발생하네요....ㅠㅠ
    혹시 뭐가 빠진거라도 있을까요??
    web.xml에 추가해줬고,,클래스도 만들었는데...
    혹시 몰라 클래스에 Systme.out.println으로 찍어도 봤는데..통과하지를 않는데...ㅜㅜ어떻게해야하는걸까요ㅜㅜ

    • 안녕하세요. 도움을 주세요ㅜㅜ님!

      블로그를 방문해주셔서 감사합니다.^^
      제가 댓글 확인이 많이 늦었습니다. ㅠ.ㅠ

      iframe 내에 다른 URL 주소의 사이트를 삽입하려고 하시는 것인지요? 삽입 전에 ajax로 확인한 다음에 그에 맞는 url을 iframe 에 삽입하려고 하시는 것이 맞나요?

      조금만 더 자세한 설명과 오류 상황을 알려주실 수 있을까요? 통과하지 않는 다는 것이 위의 SimleCORSFilter 자체를 타지 않는다는 의미이신지요? 필터가 제대로 등록되었다면 모든 서버로 오는 요청은 필터를 타야 되는 것이 맞습니다. 해당 서버쪽 코드는 ajax의 콜을 받아서 처리해주는 서버쪽에 작업을 해주셔야 합니다.

  • 감사합니다 2016.04.07 11:23 신고

    지금 스프링 프레임워크로 개발중인데 web.xml 이면 스프링의 web.xml인가요 서버의 web.xml인가요??
    동일하게 해도 안되네요..

    • 안녕하세요. 감사합니다님

      답변이 많이 늦었습니다. ㅠ.ㅠ

      스프링 또는 서버 따로 구분하는지는 모르겠지만, 서버의 실행환경 등에 대한 설정 정보를 담고 있는 WEB-INF/web.xml 경로에 있는 파일을 말하는 것입니다. 톰캣의 웹어플리케이션에 대해 기본 설정 등에 대한 정보를 설정하는 파일입니다.

      혹시 어떻게 안되시는지요?^^

      좋은 밤 되세요~

  • 크..명쾌한 강좌 감사드립니다.
    그런데 동일하게 적용했는데 해결이 안되고 있는데요
    저의 경우 IP와 포트를 이용하는데

    10.10.10.10:8180 서버 iframe 내에서 10.10.10.10:8380 서버로 호출 시 문제가 발생하는데..
    다르게 가져가야 할련지요 ㅠ

    • 안녕하세요. 혀니님^^
      블로그를 방문해 주셔서 감사합니다. 확인이 많이 늦었습니다.

      iframe을 통해서 부모와 자식간에 호출을 하는 경우에도 크로스도메인 이슈가 있는 것은 맞습니다. (포트가 달라도 크로스도메인 문제가 발생하지요)

      ip도 세팅이 되는지는 잘 모르겠지만, 어차피 도메인(ip)가 같으니iframe에서 크로스도메인 문제를 피하기 위해 부모와 자식 페이지에 window.document.domain = 'ooz.co.kr' 를 설정하여 도메인을 맞추어주는 방법이 있습니다. 이렇게 하면 자식(iframe) 페이지에서 부모 페이지의 함수를 호출하여 부모 페이지에서 서버 콜하여 처리하는 방법을 사용할 수 있을 것 같습니다.

      또는 window.postMessage()를 사용하는 방법도 있는 것 같은데, 사실 이 함수는 사용해 본적이 없네요.

      답변이 제대로 된 것인지 모르겠습니다.^^

      좋은 밤 되세요!

  • 띠딩 2016.09.09 11:11 신고

    안녕하세요


    web.xml과 SimpleCORSFilter.java를 이용하였는데요


    잘 안되어서 질문합니다

    Access-Control-Allow-Headers:Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization
    Access-Control-Allow-Methods:POST, GET, OPTIONS, DELETE
    Access-Control-Allow-Origin:*/
    Access-Control-Max-Age:3600
    Cache-Control:no-cache, no-store, max-age=0
    Content-Language:ko-KR
    Content-Type:application/json; charset=UTF-8
    Date:Fri, 09 Sep 2016 02:08:19 GMT
    Expires:Thu, 01 Jan 1970 00:00:00 GMT
    Pragma:no-cache
    Server:Jetty(9.2.11.v20150529)
    Transfer-Encoding:chunked


    response headers에 저렇게 찍혔는데


    (원인: 'Access-Control-Allow-Origin' CORS 헤더가 없음).


    에러가 저렇게 떨어지네요


    확장프로그램 corse를 이용하면 해당 영상파일을 load하고요

  • 토니아빠 2016.11.30 10:16 신고

    정말 도움 많이 되었습니다. 매번 부딪히는 문제인데 꼼수로 해결하고 있었네요. 고맙습니다.

  • JUNO 2016.12.06 12:19 신고

    아! 2시간 넘게 삽질하다가 글 보고 해결되었습니다.
    웹쪽 일은 안해봤는데, 갑자기 간단한 RESTful API 작성해야할 일이 생겨서요.

    그런데, 설명만 있고, 셋팅하는 방법(코드)는 찾기 어려웠는데, 여기에서 힌트 얻고 갑니다!

    감사합니다!! ^^

  • 감사합니다!!
    덕분에 WebAPI 구현 테스트를 좀 더 쉽게 할수있겠네요!!ㅎㅎ

    • 답변이 너무 늦었네요^^
      블로그를 방문해주셔서 감사합니다.
      도움이 된 것 같아 좋네요.^^ 좋은하루되세요!

  • River 2017.08.10 10:57

    관리자의 승인을 기다리고 있는 댓글입니다

티스토리 툴바