瀏覽器禁止跨站台 Cookie 傳送是老問題,尤以 IFrame 內嵌跨站台網頁最明顯,在 IE 時代還有「信任的網站」這招大絕,但隨著 IE 走入歷史,加上瀏覽器對於跨站台 Cookie 限制日趨嚴格,這類老寫法用起來愈來愈吃力。

先來簡單展示,假設有個設定及顯示 Cookie 的 cookie.aspx 如下:

<%@Page language="C#"%>
<script runat="server">
    string cookieName = "MyCookie";
    void Page_Load(object sender, EventArgs e)
    {
        if (Request["m"] == "set")
        {
            Response.Cookies[cookieName].Value = Request["v"];
            Response.Write("<div>Cookie is set.</div>");
            Response.Write("<script>if (window.opener) window.close();<" + "/script>");
        }
        else
        {
            Response.ContentType = "text/plain";
            Response.Write("Cookie=" + Request.Cookies[cookieName]?.Value);
        }
    }
</script>

為方便測試,我利用 Windows\System32\drivers\etc\hosts 將 my-svr-1、my-svr-2、my-svr-3 都指向 127.0.0.1,用同一台 IIS 扮演四台不同主機模擬跨站台情境。我寫了一個測試網頁 iframe-test.html 用 IFrame 內嵌 my-svr-1 到 my-svr-3 的 cookie.aspx:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Display Cross Site Cookie</title>
    <style>
        iframe {
            display: block; width: 150px; height: 50px;
        }
    </style>
</head>
<body>
    <div class="frames">
        <iframe src="http://my-svr-1/aspnet/xscookies/cookie.aspx"></iframe>
        <iframe src="http://my-svr-2/aspnet/xscookies/cookie.aspx"></iframe>
        <iframe src="http://my-svr-3/aspnet/xscookies/cookie.aspx"></iframe>
    </div>
    <script>
        document.querySelectorAll("iframe").forEach(function (iframe) {
            let title = document.createElement('div');
            title.innerText = iframe.src;
            iframe.parentNode.insertBefore(title, iframe);
        });
    </script>
</body>

</html>

如下圖所示,即使 my-svr-1 Cookie 存在,當被內嵌在 frame-test.html 時,瀏覽器不會傳送 Cookie。

改為另開視窗,Cookie 就正常了。

因應 IFrame 的跨站台 Cookie 限制,網站理應改用替代做法,無腦解法是改成另開視窗或另開頁籤,精緻一點可考慮用 postMessage 與 IFrame 跨網站網頁互動,但這需要兩邊網頁同步配合修改,工程較大多半得從長計議。

這陣子我有個需求是想從 A 站台(假設為 localhost)重設 B、C、D 站台(假設為 my-svr-1、my-svr-2、my-svr-3)的 Cookie,由於是測試環境才有的特殊狀況,不想花太多功夫處理,便想到一個簡單粗暴的解法,順便當 JavaScript 練習,裡面有些細節挺有趣,值得筆記備忘。

原理是用 window.open 逐一開啟 B、C、D 站台的 cookie.aspx?m=set 設定 Cookie 並自動關閉。這裡會遇到兩個問題:1) 按鈕動作只能合法觸發一次 window.open() 其餘兩次會被快顯封鎖器擋掉,若偵測到快顯封鎖要提示使用者開放;2) cookie.aspx 設定完會 window.close(),程式需偵測三個新開視窗都結束再執行下一步。

第一步,先寫一小段程式用 window.open() 及上回介紹的定位技巧檢視 B、C、D 站台的 Cookie 內容:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Setting Cross Site Cookie</title>
    <style>
        .frames { margin-top: 12px;}
        .frames > iframe { float: left; width: 100px; height: 50px; }
        #popupBlockerWarn { display: none; color: red; }
        .winSlots > span {
            margin: 12px; display: inline-block;
            width: 200px; height: 120px; background-color: #eee;
        }
    </style>
</head>
<body>
    <div id="popupBlockerWarn">
        請調整封鎖快顯視窗設定,允許彈出視窗
    </div>
    <button onclick="checkCookies()">Check</button>
    <span id="spnMsg"></span>
    <div class="winSlots">
    </div>
    <script>
        function showPopupBlockerWarn() {
            document.getElementById("popupBlockerWarn").style.display = "block";
        }
        var sites = [
            "http://my-svr-1/aspnet/xscookies/cookie.aspx",
            "http://my-svr-2/aspnet/xscookies/cookie.aspx",
            "http://my-svr-3/aspnet/xscookies/cookie.aspx"
        ];
        sites.forEach((u,i)=> {
            let slot = document.createElement('span');
            slot.setAttribute('id', 'slot' + i);
            document.querySelector('.winSlots').appendChild(slot);
        });
        var wins = [];
        function openWinOnElem(elemId, url) {
                let baseElem = document.getElementById(elemId);
                let baseRect = baseElem.getBoundingClientRect();
                let x = baseRect.left + window.screenX ;
                let winHeaderHeight = window.outerHeight - window.innerHeight;
                let y = baseRect.top +  window.screenY + winHeaderHeight;
                return win = window.open(url, '_blank', 
                    `popup=yes,width=${baseRect.width},height=${baseRect.height - winHeaderHeight},left=${x}px,top=${y}px`);
        }
        function checkCookies() {
            if (wins.length) return;
            sites.forEach((u, i) => {
                let win = openWinOnElem('slot' + i, u);
                if (!win) showPopupBlockerWarn();
                else wins.push(win);
            });
            let countDown = 10;
            let h = setInterval(function () {
                let msg = document.getElementById('spnMsg');
                msg.innerText = `視窗關閉倒數: ${countDown--}`;
                if (countDown <= 0) {
                    wins.filter(w => !w.closed).forEach(w => w.close());
                    wins = [];
                    msg.innerText = '';
                    clearInterval(h);
                }
            }, 1000);
        } 
    </script>
</body>

</html>

未允許快顯前,按鈕只會顯示站台 B 頁面,C、D 頁面被擋掉,網頁將提示使用者開放設定:

允許快顯後,就能一次開啟三個頁面檢查 Cookie 值:

最後,加上設定 Cookie 程式,開啟 cookie.aspx?m=set,使用 window.closed 偵測 cookie.aspx 是否執行完畢自動關閉,都完成後觸發檢查結果:

    <input type="text" placeholder="Cookie Value" id="txtValue" />
    <button onclick="setCookies()">Set</button>    
    
    <script>
        //...略
        var winOptions = 'popup=yes,status=no,scrollbars=no,resizable=no,width=100,height=50';
        function setCookies() {
            var v = document.getElementById("txtValue").value;
            sites.forEach(u => {
                var win = window.open(u + "?m=set&v=" + encodeURIComponent(v), 
                    '_blank', winOptions);
                if (!win) showPopupBlockerWarn();
                else wins.push(win);
            });
            var h = setInterval(function () {
                if (wins.length == 0) {
                    clearInterval(h);
                    checkCookies(); 
                }
                else wins = wins.filter(w => !w.closed);
            }, 100);
        }           
    </script>

成功!

Alternative way to resolve cross-site cookie setting restriction.


Comments

Be the first to post a comment

Post a comment