CORS error (ajax-xml-servlet/jsp)

2021. 8. 3. 10:27문제들

CORS error (ajax-xml-servlet/jsp)

 

발단

공공 데이터 포털 https://data.go.kr/ 에서 공휴일 정보 ( XML ) 를 REST로 받으려고 했다. 남들처럼 단순하게 XMLHttpRequest w3schools 소스로 연습하려고 했고, 환경은 apache/tomcat, jsp/servlet 에서 간단히 예제 연습하려 했다.

하지만 뜨라는 결과는 나오지 않고 CORS 에러 만 콘솔창에 떴다.

Access to XMLHttpRequest at '외부주소' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

 

Cross Origin Resource Sharing....

 

CORS 에 대한 설명은

을 보면 된다.

 

해당 글을 보고 내 나름대로 소화한 내용은....

 

Javascript 단에서 ajax를 사용하면 client 단에서 다른 server로 비동기 요청을 보내게 된다.

  • SOP(Same Origin Policy) / CORS 라는 정책이 있다. 둘은 별개다.
    • SOP : 어지간하면 느그 Origin 에서만 요청해서 써라
      • origin : protocol + domain + port(애매)
    • CORS : 다른 Origin에 요청해도 되는데 선은 지켜라
    • Access-Control-Allow-Origin : request header. 허용한 Origin 이다. * 이면 다 허용해준다.
      • 설정할때 요청할 특정 Origin을 정해주는게 낫다.
  • preflight : 미리 요청가능할지 말지 간을 보는 작업
  • 두 정책은 왜 나왔는가? :
    • 결국 웹 어플리케이션들 간의 소통 (web client - another web server )
    • 어플리케이션의 소통을 아무거나 허용하면 위험하다
      • XSS 를 위시한 사용자의 공격에 취약
    • 그럼에도 요청이 필요한 상황이 있다
    • 그래서 등록한 Origin만 요청하는것을 허용한다.

이 정도다.

저분 글은 두고두고 살펴 볼 생각이다.

 

 

삽질

되는거 안되는거 다 적용해 보았다.

나중에 비슷한 문제 생기면 일단 이거부터 시도해보고 하련다. 하다가 하나는 걸리겠지

 

header 추가

https://jang8584.tistory.com/250

https://ooz.co.kr/232

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

해당 코드를 custom filter 를 만들어서 집어넣는다.

안된다.

 

var xhttp, xmlDoc, txt, x, i;
        xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
                xmlDoc = this.responseXML;
                txt = xmlDoc;
                document.getElementById("demo").innerHTML = txt;
            }
        };
        xhttp.open("GET","요청하고싶은 주소",true);

XMLHttpRequest open 후에 추가해 보았다.

잘 안되었다.

 

서버단에서 설정

참고로 서버 단에서 cors를 지원하는 것은 상당히 간단한 것으로 알고 있습니다.
http://enable-cors.org/index.html 사이트를 참고하시기 바랍니다.
(아파치 서버 cors 지원 설정법 http://enable-cors.org/server_apache.html)

나는 apache/ tomcat 이라

여기 기웃거려가며 프로젝트 내 web.xml 에

<filter>
  <filter-name>CorsFilter</filter-name>
  <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>CorsFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

추가했다.

그래도 안된다.

 

JSONP - callback padding

나중에 참고할 링크

https://blog.kingbbode.com/26

살펴본 링크

https://m.blog.naver.com/musasin84/60208652179

https://stove99.tistory.com/10

https://pythonq.com/so/jquery/1976603

// find some demo xml - DuckDuckGo is great for this
    var xmlSource = "http://api.duckduckgo.com/?q=StackOverflow&format=xml"

// build the yql query. Could be just a string - I think join makes easier reading
    var yqlURL = [
        "http://query.yahooapis.com/v1/public/yql",
        "?q=" + encodeURIComponent("select * from xml where url='" + xmlSource + "'"),
        "&format=xml&callback=?"
    ].join("");

// Now do the AJAX heavy lifting        
    $.getJSON(yqlURL, function(data){
        xmlContent = $(data.results[0]);
        var Abstract = $(xmlContent).find("Abstract").text();
        console.log(Abstract);
    });

안됨

 

 

https://jang8584.tistory.com/250

$.ajax({
    url : "http://127.0.0.1:8080/server/data.jsp",
    dataType : "jsonp",
    jsonp : "callback",
    success : function(d){
        // d.key;
    },
    error : function(xhr){
        console.log('실패 - '+xhr);
    }

안됨 ( 하지만 이분 링크는 참고)

https://m.blog.naver.com/PostView.nhn?blogId=yohanlee0602&logNo=221427365580&categoryNo=18&proxyReferer=&proxyReferer=https:%2F%2Fwww.google.com%2F

 

네이버 블로그 rss xml 파싱 jsonp 사용하여 크로스 도메인 패스~@Access-Control-Allow-Origin

그냥 자바스크립트로 해놨는데.... xmlhttprequest post ↑↑↑↑↑↑ 요거 사용하다가 모바일, 크롬, ms e...

blog.naver.com

 <script>   
    var url= 'https://rss.blog.naver.com/yohanlee0602.xml';
    $.ajax({
      type: 'GET',
      url: "https://api.rss2json.com/v1/api.json?rss_url=" + url,
      dataType: 'jsonp',
      success: function(data) {
      console.log(data.feed.description);   
      console.log(data);   
       }
    });
       </script>

안됨

https://luckybaby.tistory.com/720

이분 것은 시도 안해보았으나 변환 하는것은 흥미로워 보인다.

 

 

해결

 

 

proxy 생성 & 서버간 통신

  • https://data.go.kr/bbs/faq/selectFaqList.do - FAQ
  • OpenAPI를 js를 이용하여 화면에 노출하려 하였으나 호출 결과 Cross Domain 에러가 발생하고 있습니다. 원인과 해결 방법을 알고 싶습니다.

Cross Domain은 Same-Origin Policy[동일근원정책]으로
Javascript에서 Ajax 사용 시 사용 문서와 동일한 도메인으로만 데이터 요청 및 전송이 가능하도록 하는 보안 정책입니다.

안녕하세요. 통계지리정보서비스를 이용해 주셔서 감사합니다.

서버쪽에는 Cross Domain 문제를 해결하기 위한 코드를 다 넣어 놓은 상태입니다.

그럼에도 클라이언트단에서 충돌이 발생한다면 브라우저 문제이므로

Ajax를 Jsonp로 구성하거나 Proxy를 만들어 서버간 통신하는 방식으로 해결하셔야 합니다.

 

 

 

sample 예제

client side Ajax

<html>    
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 
<title> openAPI 통계청:공간정보서비스과 </title>
<script src=http://sgis.kostat.go.kr/OpenAPI2/Key.do?serviceKey=발급받은키" type="text/javascript" ></script>
<script type="text/javascript">
var userURL = "http://사용자페이지URL";                
function fncGeoCode() {
    var url = userURL + "/AjaxRequest.jsp?getUrl=";
    var subURL = "http://sgis.kostat.go.kr/OpenAPI2/geocoder.do?serviceKey="+ document.getElementById("serviceKey").value;
    subURL += "&type=2";
    subURL += "&sido="+encodeURIComponent(document.getElementById("sido").value);
    subURL += "&sigungu="+encodeURIComponent(document.getElementById("sigungu").value);
subURL += "&dong="+encodeURIComponent(document.getElementById("dong").value);
subURL += "&jibun="+document.getElementById("jibun").value;
    url += encodeURIComponent(subURL);
    $.ajax({
     "url" : url,
    "type" : "GET",
    "success" : function(result) {
          if(result == null || result == ""){
        alert("해당 주소로 얻을수 있는 좌표가 없습니다. 주소값을 다시 입력하세요");
          }else{
        $.each(result, function(i,value){
            if(result.data == null ){
               if(i==0){
            $("#x_coords").attr("value",value.posX); 
$("#y_coords").attr("value",value.posY); 
            $("#address").attr("value", value.address);
              }
           }
                 });
          }
    },
    "async" : "false",
    "dataType" : "json",
    "error": function(x,o,e){
        alert(x.status + ":" +o+":"+e);    
    }
    });    
}
</script> 
</head>
<body >
    serviceKey : <input type="text" id="serviceKey" value="발급받은키"/><br />
    지번주소<br />
    시도 : <input type="text" id="sido" value="대전광역시"/><br />
    시군구 : <input type="text" id="sigungu" value="서구"/><br />
읍면동 : <input type="text" id="dong" value="월평동"/><br />
지번 : <input type="text" id="jibun" value="245"/><br />    
    <input type="button" value="GeoCode Service" onclick="fncGeoCode()"/><br />
    중부원점(TM_M)<br />
    X좌표 : <input type="text" id="x_coords" />  Y좌표 : <input type="text" id="y_coords" /><br />
    주 소 : <input type="text" id="address" /><br />
        </body>
</html>

 

Server- side Request

<%@ page language="java" import="java.io.*,java.net.*"     contentType="text/xml; charset=utf-8"  
pageEncoding="utf-8"%>
<%
    URL url = new URL(request.getParameter("getUrl"));
    URLConnection connection = url.openConnection();
    connection.setRequestProperty("CONTENT-TYPE","text/html"); 
    BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(),"utf-8"));
    String inputLine;
    String buffer = "";
    while ((inputLine = in.readLine()) != null){
         buffer += inputLine.trim();
    }
    System.out.println("buffer : " + buffer);
    in.close();
%><%=buffer%>

잘 돌아갈 뻔 했다. 하지만 이것만으로는 바로 적용이 불가능 했다.

 

URL 통한 XML 정보 받기

https://stackoverflow.com/questions/33759608/getting-xml-from-url-with-httpurlconnection-in-android

이 둘을 잘 조합해서 만들었다.

 

 

code

 

Ajax Request ( Client -> Origin(Proxy)) - 모범음식점 검색

server side 로 요청을 toss 한다. 짬 때리는 직장인의 완벽한 표본이다.

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>  
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<style>
table {
  font-family: arial, sans-serif;
  border-collapse: collapse;
  width: 100%;
}

td, th {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}

tr:nth-child(even) {
  background-color: #dddddd;
}
</style>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>OPEN API TEST</title>
<script type="text/javascript"
    src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<script type="text/javascript">
    // 입력값 정수인지 판별
    function isNumeric(str) {
      if (typeof str != "string") return false // we only process strings!  
      return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
             !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
    }


    var userURL = "http://localhost:8080/apitest";
    // 공휴일 검색 기능 
    function fnHoliday() {
        var month = document.getElementById("month").value.padStart(2,'0');
        var url = userURL + "/AjaxRequest.jsp?getUrl=";
        var subURL = "http://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo?solYear="
                + document.getElementById("year").value;
        subURL += "&solMonth=" + month;
        subURL += "&ServiceKey=" + document.getElementById("serviceKey").value;
        url += encodeURIComponent(subURL);
        console.log(url);
        $.ajax({
            "url" : url,
            "type" : "GET",
            "success" : function(result) {
                $('#result').innerHTML = result;
                console.log("success : " + result.responseXML);
                console.log(result);
                x = result.getElementsByTagName("resultMsg")[0];
                console.log(x);
                console.log(x.childNodes[0].nodeValue);
                var items = result.getElementsByTagName("item"); 
                var txt = "";

                // 테이블 구성
                if (result == null || result == "") {
                    alert("해당 주소로 얻을수 있는 좌표가 없습니다. 주소값을 다시 입력하세요");
                } else {
                    $.each(items, function(i, value) {
                        if (i == 0) {
                            txt += "<tr>";
                            txt += "<th>날짜</th>";
                            txt += "<th>명칭</th>";
                            txt += "<th>종류</th>";
                            txt += "<th>공공기관 휴일 여부</th>";
                            txt += "</tr>";     
                        }
                        if (result.data == null) {
                        }
                        txt += "<tr>";
                        txt += "<th>"+value.getElementsByTagName("locdate")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("dateName")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("dateKind")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("isHoliday")[0].childNodes[0].nodeValue+"</th>";
                        txt += "</tr>";

                    });
                    document.getElementById("result").innerHTML = txt;
                }
            },
            "async" : "true",
            "dataType" : "xml",
            "error" : function(x, o, e) {
                $('#result').innerHTML = x.responseText;
                console.log(x.responseText);
                console.log(x);
                //alert(x.status + ":" + o + ":" + e);
            }
        });
    }
    // 송파구 모범 음식점 목록 기능
    function fnRestaurant() {
        // 입력값 처리 - default 10개
        var num = document.getElementById("count").value;
        var limit = 10;
        if(isNumeric(num)){
            limit = parseInt(num,10);                
        }
        var month = document.getElementById("month").value.padStart(2,'0');
        var url = userURL + "/AjaxRequest.jsp?getUrl=";
        var subURL = "http://openAPI.songpa.seoul.kr:8088";
        subURL += "/71456d5164666f753539587572624f";
        subURL += "/xml";
        subURL += "/SpModelRestaurantDesignate";
        subURL += "/1";
        subURL += "/"+ limit + "/";
        url += encodeURIComponent(subURL);
        console.log(url);
        $.ajax({
            "url" : url,
            "type" : "GET",
            "success" : function(result) {
                $('#result').innerHTML = result;
                x = result.getElementsByTagName("SpModelRestaurantDesignate")[0];
                document.getElementById('totalCount').innerHTML = x.getElementsByTagName("list_total_count")[0].childNodes[0].nodeValue;
                var rows = result.getElementsByTagName("row"); 
                var txt = "";
                if (result == null || result == "") {
                    alert("해당 주소로 얻을수 있는 좌표가 없습니다. 주소값을 다시 입력하세요");
                } else {
                    // 테이블 구성 
                    $.each(rows, function(i, value) {
                        if (i == 0) {
                            // 헤더 구성 
                            txt += "<tr>";
                            txt += "<th>시군구코드</th>";
                            txt += "<th>지정년도</th>";
                            txt += "<th>지정번호</th>";
                            txt += "<th>신청일자</th>";
                            txt += "<th>지정일자</th>";
                            txt += "<th>업소명</th>";
                            txt += "<th>소재지도로명</th>";
                            txt += "<th>소재지지번</th>";
                            txt += "<th>허가(신고)번호</th>";
                            txt += "<th>업태명</th>";
                            txt += "<th>주된음식</th>";
                            txt += "<th>영업장면적(㎡)</th>";
                            txt += "<th>행정동명</th>";
                            txt += "<th>급수시설구분</th>";
                            txt += "<th>소재지전화번호</th>";
                            txt += "</tr>";   
                        }
                        // 테이블 내용 구성 
                        txt += "<tr>";
                        txt += "<th>"+value.getElementsByTagName("CGG_CODE")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("ASGN_YY")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("ASGN_SNO")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("APPL_YMD")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("ASGN_YMD")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("UPSO_NM")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("SITE_ADDR_RD")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("SITE_ADDR")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("PERM_NT_NO")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("SNT_UPTAE_NM")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("MAIN_EDF")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("TRDP_AREA")[0].childNodes[0].nodeValue+"</th>";
                        txt += "<th>"+value.getElementsByTagName("ADMDNG_NM")[0].childNodes[0].nodeValue+"</th>";

                        // 값이 존재하지 않을 때 예외 처리
                        var isFacility = value.getElementsByTagName("GRADE_FACIL_GBN")[0].childNodes[0];
                        //console.log(isFacility);
                        var isFacilityVal = typeof isFacility != "undefined" ? isFacility.nodeValue : "없음";                        
                        txt += "<th>"+isFacilityVal+"</th>";
                        var isTelNo = value.getElementsByTagName("UPSO_SITE_TELNO")[0].childNodes[0];
                        var isTelNoVal = typeof isTelNo != "undefined" ? isTelNo.nodeValue : "미기재";                        
                        txt += "<th>"+isTelNoVal+"</th>";                        
                        txt += "</tr>";
                    });
                    document.getElementById("restaurantResult").innerHTML = txt;
                }
            },
            "async" : "true",
            "dataType" : "xml",
            "error" : function(x, o, e) {
                $('#result').innerHTML = x.responseText;
                console.log(x.responseText);
                console.log(x);
            }
        });
    }
</script>
</head>
<body>    
    <h1>(공공데이터포털)년,월을 입력해서 공휴일을 확인</h1>
    <input type="hidden" id="serviceKey"
        value="써어비스키" />
    <br /> 년 :
    <input type="text" id="year" value="" placeholder="연도 입력(예:2019)"/>
    <br /> 월 :
    <input type="text" id="month" value="" placeholder="월 입력(예:3)"/>
    <br />
    <input type="button" value="공휴일 검색" onclick="fnHoliday()" />
    <table id='result'></table>
    <h1>(서울열린데이터광장API)송파구 모범음식점 목록</h1>
    <br /> 출력할 음식점 개수 :
    <input type="text" id="count" value="" placeholder="음식점 개수 입력(예:3)"/>
    <br />
    <input type="button" value="음식점 목록 가져오기" onclick="fnRestaurant()" />
    <br/>
    <span>송파구 내 총 음식점<h3 id='totalCount'></h3></span>
    <br/>
    <table id='restaurantResult'></table>
    <br />
</body>
</html>

 

 

Server side Request( Proxy -> another Origin)

그리 큰 차이는 없다. 마지막 부분에 buffer를 out.print 로 출력한다는 것이 소소하게 다른점.

<%@ page language="java" import="java.net.*, java.io.*" pageEncoding="UTF-8"%><%
URL url = new URL(request.getParameter("getUrl"));
URLConnection connection = url.openConnection();
connection.setRequestProperty("CONTENT-TYPE", "text/plain");
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));


String inputLine;
String buffer = "";
while ((inputLine = in.readLine()) != null) {
    buffer += inputLine.trim();
}
//System.out.println("buffer : " + buffer);
in.close();
response.setContentType("application/xml");
out.print(buffer);

%>

 

 

그외 발생한 자잘한 것들

  • Uncaught ReferenceError: $ is not defined (ajax)
    • script tag를 열면 닫는 태그도 똑같이 넣어준다.
    • <script ~~~ ></script>
  • JQuery - $ is not defined
    • ReferenceError: $ is not defined (ajax)
    • jquery 를 추가해준다.
    • <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
  • 200 parserrror invalid
    • dataType 확인
      • 내 경우엔 xml 이라서 xml 로 설정
  • XML declaration allowed only at the start of the document

 

happy 하다