본문 바로가기

개발&컴퓨터/JAVA & SPRING

CORS 크로스 도메인 이슈 (No 'Access-Control-Allow-Origin' header is present on the requested resource)

반응형

웹 사이트 개발 시, 주요한 이슈중의 하나를 꼽자면 크로스 도메인(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개의 도메인에 대해서만 크로스 도메인을 허용하겠다는 의미입니다.

 

 

출처/참고 :

* https://gist.github.com/zhentao/5707286 

* https://spring.io/guides/gs/rest-service-cors/

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

반응형