HTML5 API - CORS(Cross Oragin Resource Sharing)


초기 Ajax 통신은 같은 도메인 간 컨텐츠 제어만 가능했었습니다. 


하지만 HTML5 명세의 "XMLHttpRequest Level 2" 사용 시 타 도메인 간 자원 공유

(Cross Oragin Resource Sharing(CORS))가 가능해 졌습니다.


즉, HTTP 요청 헤더에는 기존 통신 방식(Ajax)에 없었던 Origin 헤더가 포함되며, 이 헤더를 통해 송신자 측 도메인을 해당 서버에게 알려줍니다.


또한, 이 헤더는 브라우저에 의해 보호되며, 애플리케이션 코드로 변경할 수 없고, 이전 포스트인 "PostMessage API"에서 언급한 e.origin 속성과 동일한 기능을 합니다.



마지막으로 Origin 헤더의 또 한가지 특성으로는 이전 URL의 모든 경로를 포함하는 Reffer 헤더와는 달리 이 헤더는 브라우저로부터 무조건 전송됩니다.



P.S: Reffer 헤더는 전송 방식에 따라 헤더에 포함되지 않기도 합니다.(간단히 데이터 전송 시(GET, POST등..)는 포함되며, 기본적인 링크 이동 시에서는 포함되지 않습니다.)



이전 포스트에서 서버 측 "CORS"를 다루는 방법에 대해 설명 드렸습니다.


또한, 아래 코드는 이전 내용에서 다루지 못한 "CORS" 클라이언트 설계 부분입니다.




"CORS" 전송을 위한 수정된 AJAX API 모듈:


  1. var Ajax = (function (win, doc) {
  2.  
  3.     var ua = window.navigator.userAgent.toLowerCase();
  4.  
  5.     return { request: function (opt) { return new request(opt); } };
  6.  
  7.     // 요청
  8.     function request(opt) {
  9.  
  10.         this.options = {
  11.  
  12.             url: '',
  13.             type: 'html',
  14.             method: 'post',
  15.             headers: {},
  16.             data: {},
  17.             iscors: false,
  18.             onprogress: function () { ; },
  19.             onerror: function () { ; },
  20.             callback: function () { ; }
  21.         };
  22.  
  23.         extend.call(this.options, opt);
  24.  
  25.         // 크로스 도메인 전송 여부
  26.         this.options.iscors = isCORS.call(this.options);
  27.  
  28.         start.call(this.options);
  29.  
  30.         return this;
  31.     };
  32.  
  33.  
  34.     function start() {
  35.  
  36.         if (!this.url) return false;
  37.  
  38.         var xhr = getXHR.call(this)
  39.         , params = getParamsSerialize(this.data);
  40.  
  41.         // 크로스 도메인 전송 시
  42.         if (this.iscors) appendCORSEvents.call(this, xhr);
  43.  
  44.         var method = this.method = this.method.toLowerCase();
  45.  
  46.         var url = (method === 'get' && params) ? this.url + '?' + params + '&s=' + encodeURIComponent(new Date().toUTCString()) : this.url;
  47.  
  48.         xhr.open(method, url, true);
  49.  
  50.         // HTTP Header 추가
  51.         setHeaders.call(xhr, this.headers);
  52.  
  53.         (function ($this) { xhr.onreadystatechange = function () { handler.call($this, xhr); }; })(this);
  54.  
  55.         if (method === 'get' || method === 'head' || method === 'put' || method === 'delete' || method === 'options') xhr.send(null);
  56.         else if (method === 'post') xhr.send(params + '&s=' + encodeURIComponent(new Date().toUTCString()));
  57.     }
  58.  
  59.  
  60.     // xhr 객체 가져오기
  61.     function getXHR() {
  62.         /*
  63.         ie5.5+: MiCORSoft.XMLHTTP,
  64.         ie6+: Msxml2.XMLHTTP,
  65.         ie7+ or 표준 브라우저: XMLHttpRequest(), XDomainRequest()(cors 지원 브라우저)
  66.         표준 브라우저: XMLHttpRequest()
  67.         */
  68.  
  69.         return !window.XMLHttpRequest ? new ActiveXObject(ua.indexOf('msie 5') > -1 ? 'MiCORSoft.XMLHTTP' : 'Msxml2.XMLHTTP')
  70.         : this.iscors && window.XDomainRequest ? new window.XDomainRequest()
  71.         : window.XMLHttpRequest ? new XMLHttpRequest() : null;
  72.     };
  73.  
  74.     // CORS 사용 여부
  75.     function isCORS() {
  76.  
  77.         var url = this.url.replace(/(https:\/\/|http:\/\/)/, '');
  78.  
  79.         if (url.indexOf('/') > -1) url = url.substr(0, url.indexOf('/'));
  80.         else url = url.substr(0, url.length);
  81.        
  82.         return top.location.host !== url;
  83.     }
  84.  
  85.     // CORS 사용 시 해당 이벤트 핸들러 할당
  86.     function appendCORSEvents(x) {
  87.  
  88.         var that = this;
  89.  
  90.         if (window.XDomainRequest) {
  91.             x.onload = function () { that.callback.apply(x, [getXhrData.call(that, x), x.status]); };
  92.             x.onprogress = this.onprogress;
  93.             x.onerror = this.onerror;
  94.  
  95.  
  96.         }
  97.         else if (typeof x.withCredentials !== 'undefined') {
  98.             bind(x, 'load', function () { that.callback.apply(x, [getXhrData.call(that, x), x.status]); });
  99.             //bind(x, 'progress', function (e) { that.onprogress.apply(x, [e, e.total, e.loaded, (parseFloat(e.loaded / e.total) * 100), e.lengthComputable]) });
  100.             bind(x.upload, 'progress', function (e) { that.onprogress.apply(x, [e, e.total, e.loaded, (parseFloat(e.loaded / e.total) * 100), e.lengthComputable]) });
  101.             bind(x, 'error', this.onerror);
  102.         }
  103.     }
  104.  
  105.     // 파라메터 직렬화
  106.     function getParamsSerialize(data) {
  107.  
  108.         var ret = [];
  109.  
  110.         for (var p in data) ret.push(p + '=' + encodeURIComponent(data[p]));
  111.  
  112.         return ret.join('&');
  113.     };
  114.  
  115.     // 요청 헤더 추가
  116.     function setHeaders(headers) {
  117.         if (this.iscors) headers['X-Requested-With'] = 'XMLHttpRequest';
  118.         if (this.setRequestHeader) for (var h in headers) if (headers[h] !== '') this.setRequestHeader(h, headers[h]);
  119.     };
  120.  
  121.     // 응답 헨들러
  122.     function handler(x) {
  123.  
  124.         if (!this.iscors && x.readyState === 4) {
  125.             if (error(x.status)) alert('request Error status:' + x.status);
  126.             else this.callback.apply(x, [getXhrData.call(this, x), x.status]);
  127.         }
  128.     };
  129.  
  130.  
  131.     // 서버 에러 유/무
  132.     function error(s) {
  133.  
  134.         return !s && window.location.protocol === 'file:' ? false : s >= 200 && s < 300 ? false : s === 304 ? false : true;
  135.     };
  136.  
  137.     // 수신받은 결과값 가공 함수
  138.     function getXhrData(x) {
  139.  
  140.         var contentType = ''
  141.             , xml = false
  142.             , json = false;
  143.  
  144.         if (x.getResponseHeader){
  145.             contentType = x.getResponseHeader('content-type')
  146.             xml = contentType && contentType.indexOf('xml') > -1
  147.             json = contentType && contentType.indexOf('json') > -1;
  148.         }
  149.  
  150.         var type = this.type.toLowerCase();
  151.  
  152.         if (xml && type === 'xml') return x.responseXML;
  153.         else if (json && type === 'json') return eval('(' + x.responseText + ')');
  154.         else if (type === 'text') return x.responseText.replace(/<(\/)?([a-zA-Z]*)(\s[a-zA-Z]*=[^>]*)?(\s)*(\/)?>/g, '');
  155.         else return x.responseText;
  156.  
  157.     };
  158.  
  159.     // 객체 상속 함수
  160.     function extend() {
  161.  
  162.         var target = this
  163.         , opts = []
  164.         , src = null
  165.         , copy = null;
  166.  
  167.         for (var i = 0, length = arguments.length; i < length; i++) {
  168.  
  169.             opts = arguments[i];
  170.  
  171.             for (var n in opts) {
  172.                 src = target[n];
  173.                 copy = opts[n];
  174.  
  175.                 if (src === copy) continue;
  176.                 if (copy) target[n] = copy;
  177.             }
  178.         }
  179.     };
  180.  
  181. })(window, document);



실행코드:


  1. function ajax()
  2. {
  3.  
  4.     Ajax.request({
  5.         //url: 'http://m.fpscamp.com/main/index.aspx',
  6.         //url: 'http://m.fpscamp.com',
  7.         url: 'http://sof.fpscamp.com/sof.aspx',
  8.         type: 'text',
  9.         method: 'post',
  10.         headers: {
  11.             'content-type': 'application/x-www-form-urlencoded'
  12.         },
  13.         data: {
  14.             id: 'id1'
  15.         },
  16.         onprogress: function (e, total, loaded, per, computable) {
  17.             alert(e); // 이벤트
  18.             alert(total); // 전체 데이터 크기
  19.             alert(loaded); // 전송된  데이터 크기
  20.             alert(per); // 전송된 데이터 크기(percent)
  21.             alert(computable); // 전체 데이터 크기를 알고 있는지 여부
  22.         },
  23.         onerror: function () {
  24.             alert('onerror');
  25.         },
  26.         callback: function (data, status) {
  27.             alert(data);
  28.             //alert(status);
  29.         }
  30.     });
  31. };
  32.  
  33. // 이벤트 할당
  34. function bind(elem, type, handler, capture) {
  35.     type = typeof type === 'string' && type || '';
  36.     handler = handler || function () { ; };
  37.  
  38.     if (elem.addEventListener) {
  39.         elem.addEventListener(type, handler, capture);
  40.     }
  41.     else if (elem.attachEvent) {
  42.         elem.attachEvent('on' + type, handler);
  43.     }
  44.  
  45.     return elem;
  46. };






- 아래는 이전 모듈에서 수정 및 추가 된 부분입니다.



CORS 전송 유/무를 가리기 위해 "로컬 도메인"과 사용자로부터 전달된 "요청 도메인"을 비교하는 함수입니다.


  1. // CROS 사용 여부
  2. function isCORS(){
  3.  
  4.     var url = this.url.replace(/(https:\/\/|http:\/\/)/, '');
  5.  
  6.     if (url.indexOf('/') > -1) url = url.substr(0, url.indexOf('/'));
  7.     else url = url.substr(0, url.length);
  8.        
  9.     return top.location.host !== url;
  10. }




CORS 전송 시 IE8+ 및 각종 표준 브라우저의 XHR 객체에 이벤트 핸들러를 할당합니다.


  1. // CORS 사용 시 해당 이벤트 핸들러 할당
  2. function appendCORSEvents(x) {
  3.  
  4.     var that = this;
  5.  
  6.     if (window.XDomainRequest) {
  7.         x.onload = function () { that.callback.apply(x, [getXhrData.call(that, x), x.status]); };
  8.         x.onprogress = this.onprogress;
  9.         x.onerror = this.onerror;
  10.  
  11.  
  12.     }
  13.     else if (typeof x.withCredentials !== 'undefined') {
  14.         bind(x, 'load', function () { that.callback.apply(x, [getXhrData.call(that, x), x.status]); });
  15.         //bind(x, 'progress', function (e) { that.onprogress.apply(x, [e, e.total, e.loaded, (parseFloat(e.loaded / e.total) * 100), e.lengthComputable]) });
  16.         bind(x.upload, 'progress', function (e) { that.onprogress.apply(x, [e, e.total, e.loaded, (parseFloat(e.loaded / e.total) * 100), e.lengthComputable]) });
  17.         bind(x, 'error', this.onerror);
  18.     }
  19. }



IE8+ 브라우저에서는 xhr.setRequestHeader() 메서드를 지원하지 않으며, CORS 전송 시 커스텀 헤더인 "X-Requested-With" 헤더를 사용할 수 없습니다.    


  1. // 요청 헤더 추가
  2. function setHeaders(headers) {
  3.     if (this.iscros) headers['X-Requested-With'] = 'XMLHttpRequest';
  4.     if (this.setRequestHeader) for (var h in headers) if (headers[h] !== '') this.setRequestHeader(h, headers[h]);
  5. };



setHeaders() 메서드와 마찬가지로 IE8+ 브라우저에서는 xhr.getResponseHeader () 메서드 또한 지원하지 않습니다.


  1. // 수신받은 결과값 가공 함수
  2. function getXhrData(x) {
  3.  
  4.     var contentType = ''
  5.         , xml = false
  6.         , json = false;
  7.  
  8.     if (x.getResponseHeader){
  9.         contentType = x.getResponseHeader('content-type')
  10.         xml = contentType && contentType.indexOf('xml') > -1
  11.         json = contentType && contentType.indexOf('json') > -1;
  12.     }
  13.  
  14.     var type = this.type.toLowerCase();
  15.  
  16.     if (xml && type === 'xml') return x.responseXML;
  17.     else if (json && type === 'json') return eval('(' + x.responseText + ')');
  18.     else if (type === 'text') return x.responseText.replace(/<(\/)?([a-zA-Z]*)(\s[a-zA-Z]*=[^>]*)?(\s)*(\/)?>/g, '');
  19.     else return x.responseText;
  20.  
  21. };