Cross Document Messageing

Cross Document Messaging은 iframe, 탭, 윈도우 간의 안전한 cross-origin 통신을 가능하게 해준다. 또한 postMessage API를 메세지를 주고받기 위한 표준화된 방법으로서 제공하고 있다. 아래 예제에서 보여줄 것과 같이 postMessage API를 통해서 메세지를 주고 받는 것은 매우 쉽다.

    chatFrame.contentWindow.postMessage("Hello, world", "http://www.example.com");

메세지를 수신하기 위해서는 페이지에 이벤트 핸들러만 추가시켜 주면 된다. 이벤트 핸들러를 통해 메세지가 도착했을 때, origin을 확인할 수 있고 메세지로 무엇을 할지 말지 결정할 수 있다. 아래는 임으로 정의한 messageHandler라는 함수를 이용하여 메세지를 처리하는 과정을 나타내고 있다.

      window.addEventListener("message",messageHandler,true);
      function messageHandler(e) {
        switch(e.origin) {
          case "friend.example.com":
          // 메세지 처리
          processMessage(e.data);
          break;
        default:
          // 메세지의 origin을 인식할 수 없습니다.
          // 메세지 무시
        }
      }

메세지 이벤트는 dataorigin속성을 가지고 있는 DOM 이벤트이다. data속성은 메세지를 주는 곳이 보내준 실제 메세지이고 origin은 메세지를 보낸 곳이다.origin속성을 사용하여 신뢰할 수 없는 사이트에서 오는 메세지를 차단할 수 있다. 즉, 신뢰할 수 있는 목록의 사이트들만 쉽게 확인할 수 있는 것이다.

아래 그림에서 보는 것과 같이 http://chat.example.net에서 호스팅 되는 채팅위젯 iframe과 http://portal.example.com에서 호스팅되고 있는 채팅위젯 iframe을 포함하고 있는 부모 HTML 페이지가 postMessage API를 통해 통신하는 것을 볼 수 있다. (두 사이트는 .net과.com으로 다른 origin이다.)

그림1

위 예에서 채팅위젯은 iframe에 들어가 있다. 따라서 채팅위젯은 부모 페이지에는 직접적인 접근을 갖지는 못한다. 채팅위젯이 메세지를 받았을 때, 사용자에게 새로운 메세지를 받았다는 것을 메인 페이지에 알리기 위해서 postMessage를 사용할 수 있다. 이와 비슷하게, 메인 페이지(iframe을 보함하는 부모페이지:portal.example.com)도 역시 사용자의 상태정보를 데이터를 채팅위젯 페이지에 보낼 수 있다. 메인 페이지와 위젯 iframe페이지 모두 서로를 신뢰할 수 있는 origin으로 설정하므로써 서로의 메세지를 전달 받을 수 있다.

postMessage가 소개되기 전에, iframe들 간의 통신은 때때로 direct scripting을 통해서 이루어 질 수 있었다. 한 페이지에서 동작하는 스크립트가 다른 문서의 정보를 가져오는 것이다. 이것은 아마도 보안 제약으로 인해 허용되지 않았다. 이러한 직접적인 프로그램적인 접근 대신에, postMessage는 자바스크립트 컨텍스트끼리 메세지를 비동기적으로 주고 받는 방법을 제공한다. postMessage없이 크로스 도메인 통신을 하면, 브라우저가 cross-site 스크립팅 공격을 막기 위해 보안에러를 발생시킨다.

postMessage는 같은 origin을 가진 문서간에 통신을 위해서 사용될 수도 있지만, 브라우저의 same-domain policy에 의해 허용될지 않은 경우의 통신 방법으로 특히 유용하다. 하지만 postMessage가 일관성과 사용하기 쉬운 API이기 때문에 같은 origin을 가지고 있는 경우에도 사용한다. postMessage API는 HTML5 Web Workers와 같이 자바스크립트 컨텍스트 내의 통신이 필요할 때마다 사용한다.

Origin 보안에 대한 이해

HTML5는 origin이라는 개념을 도입하므로써 도메인 보안을 명확히하고 개선하였다. origin은 웹에서 신뢰할 수 있는 연결을 모델링 하기 위해 사용되는 address의 부분집합이다. origin은 scheme, host, port로 구성된다. 예를 들어 https://www.example.comhttp://www.example.com과 다른 origin이다. 왜냐하면 httpshttp라는 다른 scheme을 가지고 있기 때문이다. origin에서 path(경로)는 고려하지 않는다. 따라서 http://www.example.com/index.htmlhttp://www.example.com/page2.html은 path만 다르기 때문에 같은 origin을 갖는 것이다.

HTML5는 origin의 직렬화에 대해 정의하였다. 문자열 형식으로 API와 프로토콜에서 origin을 참조할 수 있다. 이것은 XMLHttpRequest를 이용한 cross-origin HTTP 요청과 WebSocket에서 매우 필수적이다.

Cross-origin 통신은 송신자를 origin으로 확인한다. 이것은 수신자가 신뢰할 수 없는 origin으로부터 오는 메세지나 예상되지 않은 곳에서 오는 메세지를 무시할 수 있게 한다. 더불어 어플리케이션은 이벤트 리스너를 추가하여 선택적으로 메세지를 받아야 한다. 이러한 이유들로 수상한 어플리케이션으로부터 메세지의 간섭 위험이 사라진다.

postMessage를 위한 보안지침은 메세지는 반드시 예상되지 않거나 원치 않는 origin 페이지에 전달되지 말아야 한다는 것이다. 만약 송신자가 postMessage를 호출하는 창이 특정 origin을 가지고 있지 않으면(예를 들어, 사용자가 다른 사이트를 탐색하는 경우) 브라우저는 메세지를 전달하지 않을 것이다.

이와 유사하게 메세지를 받을 때도 송신자의 origin은 메세지에 포함되어 전달된다. 메세지의 origin은 브라우저에 의해 제공되기 때문에 속일 수 없다. 이것은 수신하는 쪽에서 어떤 메세지를 처리하고 어떤 메세지를 무시할지 결정할 수 있게 해준다. 당신은 화이트리스트를 관리하여 신뢰할 수 있는 origin의 문서들의 메세지들만 처리할 수 있다.

postMessage API 사용하기

브라우저 호환성 테스트

postMessage를 호출하기 전에 브라우저의 지원여부를 확인하는 것은 좋은 생각이다. 아래의 예제는 postMessage를 브라우저의 지원하는지 확인하는 방법 중에 하나를 보여주고 있다.

    if(typeof window.postMessage === "undefined") {
        // 브라우저에서 postMessage를 지원하지 않습니다.
    }

메세지 보내기

메세지를 전달하기 위하여, 아래의 예제와 같이 타겟이 되는 window 객체에 postMessage를 호출한다.

    window.postMessage("Hello, world", "portal.example.com");

첫 번째 인자 값은 보내질 값을 나타낸다. 두 번째 인자 값은 목표로 하는 타겟의 origin이다. 메세지를 iframe에 보내기 위해서, postMessage의 contentWindow에 아래 예제와 같이 호출한다.

    document.getElementsByTagName("iframe")[0].contentWindow.postMessage("Hello, world", "chat.example.net");

메세지 이벤트 전달 받기

window 객체의 이벤트 리스너를 통해 스크립트는 아래의 코드와 같이 메세지를 전달 받을 수 있다. 이벤트 리스너 함수에서 메세지를 전달받는 어플리케이션은 메세지은 허용할지 무시할지 결정할 수 있다.

    function checkWhiteList(origin) {
        for(var i=0; i<originWhiteList.length; i++) {
            if(origin === originWhiteList[i]) {
                return true;
            }
        }
        return false;
    }
    
    function messageHandler(e) {
        if(checkWhiteList(e.origin)) {
            processMessage(e.data);
        } else {
            // 알 수 없는 origin으로부터 온 메세지는 무시한다.
        }
    }
    
    window.addEventListener("message", messageHandler, true);

postMessage API를 사용한 어플리케이션 구현

앞서 말한 포탈 어플리케이션에 cross-origin 채팅 위젯을 만든다고 한다고 가정하자. 아래 그림과 같이 Cross Document Messaging을 활용하여 채팅 위젯을 만들 수 있다

2

이 예제를 통해 우리는 포탈 페이지가 서드파티의 위젯을 iframe에 어떻게 넣는지 알았다. 우리의 예제는 http://chat.example.net의 채팅 위젯 하나였다. 포탈 페이지와 위젯은 postMessage를 통하여 통신할 수 있었다. 예제에서 채팅 위젯 iframe은 사용자에게 알림을 전달하기위해 웹 페에지에 타이틀을 깜박 거렸다. 이것은 백그라운에서 이벤트를 받는 어플리케이션들에서 찾아볼 수 있는 일반적인 UI 기술이다. 채팅위젯이 부모 페이지와는 다른 origin에서 서비스되는 iframe에 고립되어 있기 때문에, 부모 페이지의 제목을 바꾸는 것은 보안 위반이다. 대신에 채팅 위젯은 postMessage를 이용하여 부모 페이지에게 알림 메세지를 전달했다.

예제 에서 포탈 페이지는 사용자가 자신의 상태(status)를 바꾸었다고 iframe에게 메세지를 전달한다. postMessage를 이러한 방식으로 사용하므로써 이와 같은 포탈은 채팅위젯과 같이 결합된 페이지 어플리케이션에게 메세지를 전달한다. 물론 목표로 하는 origin은 화이트 리스트를 체크하여 메세지를 선택적으로 받는다. 따라서 메세지가 유출이 사고나 고의적인 의도에 의해 이루어질 수 없다.

자세한 설명을 위해 postMessagePotal.html과 postMessageWidget.html을 생성했다.

포털 페이지 만들기

첫 번째로 다른 origin에서 호스팅 되는 채팅 위젯 iframe을 포탈 페이지에 추가한다.

<iframe id="widget" src="http://chat.example.net:9999/postMessageWidget.html"></iframe>

그 다음은 messageHandler라는 이벤트 리스너를 추가하여 채팅 위젯으로부터 오는 메세지 이벤트를 가져오는 것이다. 아래에서 보는 예제 코드와 같이, 위젯은 포털에게 제목표시를 깜박거리게 할 사용자 알림을 할지를 확인하게 된다. 채팅 위젯으로부터 메세지가 오는지 확인하기 위해 메세지의 origin을 확인한다. 만약 메세지가 http://chat.example.net:9999에서 오지 않는다면 포탈페이지는 메세지를 무시할 것이다.

    var targetOrigin = "http://chat.example.net:9999";
    
    function messageHandler(e) {
        if(e.origin == targetOrigin){
            notify(e.data);
        } else {
            // 다른 도메인에서 온 메세지는 무시한다.
        }
    }

그 다음은 채팅 위젯과 통신할 수 있는 함수를 만드는 것이다. 포털 페이지에 속해 있는 위젯 iframe에게 상태 업데이트를 보내기 위해 postMessage를 사용한다. 실제 라이브 채팅 어플리케이션에서 이것은 사용자 상태(온라인, 부재중 등등)를 알리기 위해 사용될 수 있다.

    function sendString(s) {
        document.getElementById("widget").contentWindow.postMessage(s, targetOrigin);
    }

채팅위젯 페이지 만들기

첫 번째로 포털 페이지로부터 오는 메세지를 전달받기 위해 messageHandler라는 이벤트 리스너를 추가한다. 아래 예제코드와 같이 채팅 위젯은 상태 변화 메세지를 전달받게 된다. 메세지가 포탈페이지로부터 오는지 확인하기 위해 origin을 확인할 것이다. 만약 메세지가 http://portal.example.com:9999로부터 오지 않는다면 위젯페이지는 이 메세지를 무시할 것이다.

    var targetOrigin = "http://portal.example.com:9999";
    
    function messageHandler(e) {
        if(e.origin === "http://portal.example.com:9999") {
            document.getElementById("status").textContent = e.data;
        } else {
            // 다른 origin으로 부터 온 메세지는 무시한다.
        }
    }

그 다음으로 포탈페이지와 통신할 함수를 추가한다. 위젯은 포탈에게 새로운 채팅 메세지가 받아지면 postMessage를 통하여 사용자에게 알림을 할지 아래 예제와 같이 묻는다.

    function sendString(s) {
        window.top.postMessage(s, targetOrigin);
    }

최종 코드

postMessagePortal.html

<!DOCTYPE html>
<html>
    <head>
        <title>Portal [http://portal.example.com:9999]</title>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="styles.css">
        <style>
            iframe {
                height: 400px;
                width: 800px;
            }
        </style>
        <link rel="icon" href="http://apress.com/favicon.ico">
        <script>
            var defaultTitle = "Portal [http://portal.example.com:9999]";
            var notificationTimer = null;
            
            var targetOrigin = "http://chat.example.net:9999";
            
            function messageHandler(e) {
                if(e.origin == targetOrigin){
                    notify(e.data);
                } else {
                    // 다른 도메인에서 온 메세지는 무시한다.
                }
            }
            
            function sendString(s) {
                document.getElementById("widget").contentWindow.postMessage(s, targetOrigin);
            }
            
            function notify(message) {
                stopBlinking();
                blinkTitle(message, defaultTitle);
            }
            
            function stopBlinking() {
                if(notificationTimer !== null){
                    clearTimeout(notificationTimer);
                }
                document.title = defaultTitle;
            }
            
            function blinkTitle(m1, m2){
                  document.title = m1;
                // setTimeout함수의 3번 째 인수부터는 콜백함수의 인자 값으로 들어간다.
                // 여기서는 blickTitle 함수의 인자 값으로 m2, m1을 사용하는 것이다.
                notificationTimer = setTimeout(blinkTitle, 1000, m2, m1);
            }
            
            function sendStatus() {
                var statusText = document.getElementById("statusText").value;
                sendString(statusText);
            }
            
            function loadDemo() {
                document.getElementById("sendButton").addEventListener("click", sendStatus,true);    
                document.getElementById("stopButton").addEventListener("click", stopBlinking,true);
                sendStatus();    
            }
            window.addEventListener("load", loadDemo, true);
            window.addEventListener("message", messageHandler, true);
            
        </script>
    </head>
    <body>
        <h1>Cross-Origin 포탈</h1>
        <p><b>Origin</b>: http://portal.example.com:9999</p>
        Status <input type="text" id="statusText" value="Online">
        <button id="sendButton">Change Status</button>
        <p>이것은 포텔 페이지에 포함되어 있는 위젯 iframe에 상태를 업데이트 하기 위해 postMessage를 이용한다.</p>
        <iframe id="widget" src="http://chat.example.net:9999/postMessageWidget.html"></iframe>
        <p>
            <button id="stopButton">페이지의 타이틀이 깜박거리는 것을 중지</button>
        </p>                
    </body>
</html>

postMessageWidget.html

<!DOCTYPE html>
<html>
    <head>
        <title>Chat Widget</title>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="styles.css">
        <script>
            var targetOrigin = "http://portal.example.com:9999";
            
            function messageHandler(e) {
                if(e.origin === "http://portal.example.com:9999") {
                    document.getElementById("status").textContent = e.data;
                } else {
                    // 다른 origin으로 부터 온 메세지는 무시한다.
                }
            }
            
            function sendString(s) {
                window.top.postMessage(s, targetOrigin);
            }
            
            function loadDemo() {
                document.getElementById("actionButton").addEventListener("click",
                    function () {
                        var messageText = document.getElementById("messageText").value;
                        alert("test");
                        sendString(messageText);
                    }, true);
            }
            
            window.addEventListener("load", loadDemo, true);
            window.addEventListener("message", messageHandler, true);
        </script>
    </head>
    <body>
        <h1>위젯 iframe</h1>
        <p><b>Origin</b>: http://chat.example.net:9999</p>
        <p>포탈에 포함되어 있는 Status를 다음과 같이 설정: <strong id="status"></strong></p>
        <div>
            <input type="text" id="messageText" value="Widget notification.">
            <button id="actionButton">Notification 보내기</button>
        </div>
        <p>이것은 포탈 사이트가 사용자에게 알리지 물을 것이다. 포털 사이트의 제목표시줄은 제목을 깜박거리며 번갈아 가며 반복적으로 보여줄 것이다. 만약 메세지가  http://chat.example.net:9999 이외에서 온다면 포탈 페이지는 이 메세지들을 무시할 것이다.</p>
    </body>
</html>

동작하는 어플리케이션 만들기(서버 세팅)

    1. C:\WINDOWS\system32\drivers\etc 경로로 가서 “hosts” 파일을 메모장으로 엽니다
image
    2. 아래 그림과 같이 127.0.0.1과 chat.example.net, portal.example.com을 적어 넣고 저장합니다image
        3. http://python.org/download/를 방문하여 Python 2.7.1 Python2.7.1 Windows installer를 다운로드 합니다.image
        4. 파이썬이 설치 된 폴더에 책을 보고, 작성하신 postMessagePortal.html과 postMessageWidget.html 파일을 넣습니다
      image
        5. “시작”>”실행”>cmd를 입력하여 커맨드 창을 실행시킨 후 cd C:\Python27을 입력합니다.
      image
        6. 커맨드 창에 python –m SimpleHTTPServer 9999를 입력합니다.

      image

        7. 위의 설정을 다 한후, postMessage API를 지원하는 브라우저의 주소창에 http://portal.example.com:9999/postMessagePortal.html을 입력합니다.
      Posted by 강부자아들
      ,