向 XAuth 學習:跨域 localStorage
當新的開源 JavaScript 實用程序發佈時,我通常不會太興奮。這可能是我的憤世嫉俗,但總的來說,我覺得太陽底下幾乎沒有什麼新東西是真正有用的。這些實用程序中的大多數都是其他實用程序的仿冒品,或者太大而無法實際使用。然而,當我第一次接觸 XAuth 時,一種有點刺痛的興奮感席捲了我。我在查看源代碼時的第一個連貫的想法:這絕對是絕妙的。
什麼是 XAuth?
我不想花太多時間確切地解釋 XAuth 是什麼,因為您可以自己閱讀文檔以找到最本質的細節。簡而言之,XAuth 是一種在瀏覽器中共享第三方認證信息的方式。不是每個應用程序都需要通過服務的授權過程,而是使用 XAuth 將此信息存儲在您的瀏覽器中,並使其可供 Web 開發人員使用。這意味著當您登錄 Yahoo! 時,該網站可以為您提供更相關的體驗。無需發出任何額外請求即可確定您是否已登錄。您可以在 Meebo 博客上閱讀有關 XAuth 的更多信息。
最酷的部分
這篇文章實際上很少涉及 XAuth 的使用,而更多地涉及實現。 Meebo 的聰明人所做的實際上是在瀏覽器中創建一個數據服務器。他們這樣做的方式是結合了跨文檔消息傳遞和 <a href="http://hacks.mozilla.org/2009/06/localstorage/">localStorage</a>
的強大功能 .從 localStorage
綁定到單一來源,您無法直接訪問由不同域存儲的數據。這使得僅使用此 API 時完全不可能跨域共享數據(請注意與 cookie 的區別:您可以指定哪些子域可以訪問數據,但不能指定完全不同的域)。
由於主要限制是 localStorage
的同源策略 ,規避該安全問題是實現數據自由的途徑。跨文檔消息傳遞功能旨在允許在來自不同域的文檔之間共享數據,同時仍然是安全的。 XAuth 中使用的兩部分技術非常簡單,包括:
- 服務器頁面 – 有一個託管在 http://xauth.org/server.html 的頁面充當“服務器”。它唯一的工作就是處理
localStorage
的請求 .該頁面使用縮小的 JavaScript 盡可能小,但您可以在 GitHub 上查看完整源代碼。 - JavaScript 庫 – 單個小腳本文件包含公開功能的 JavaScript API。此 API 需要包含在您的頁面中。當您第一次通過 API 發出請求時,它會創建一個
iframe
並將其指向服務器頁面。加載後,數據請求通過iframe
通過跨文檔消息傳遞到服務器頁面。完整的源代碼也可以在 GitHub 上找到。
雖然 XAuth 的目標是提供身份驗證服務,但同樣的基本技術可以應用於任何數據。
通用技術
假設您的頁面在 www.example.com 上運行,並且您希望獲取一些存儲在 localStorage
中的信息 對於 foo.example.com。第一步是創建一個 iframe,它指向 foo.example.com 上充當數據服務器的頁面。該頁面的工作是處理傳入的數據請求並將信息傳回。一個簡單的例子是:
<!doctype html>
<!-- Copyright 2010 Nicholas C. Zakas. All rights reserved. BSD Licensed. -->
<html>
<body>
<script type="text/javascript">
(function(){
//allowed domains
var whitelist = ["foo.example.com", "www.example.com"];
function verifyOrigin(origin){
var domain = origin.replace(/^https?:\/\/|:\d{1,4}$/g, "").toLowerCase(),
i = 0,
len = whitelist.length;
while(i < len){
if (whitelist[i] == domain){
return true;
}
i++;
}
return false;
}
function handleRequest(event){
if (verifyOrigin(event.origin)){
var data = JSON.parse(event.data),
value = localStorage.getItem(data.key);
event.source.postMessage(JSON.stringify({id: data.id, key:data.key, value: value}), event.origin);
}
}
if(window.addEventListener){
window.addEventListener("message", handleRequest, false);
} else if (window.attachEvent){
window.attachEvent("onmessage", handleRequest);
}
})();
</script>
</body>
</html>
這是我建議的最小實現。關鍵函數是handleRequest()
, message
時調用 在窗口上觸發事件。由於我在這裡沒有使用任何 JavaScript 庫,因此我需要手動檢查附加事件處理程序的適當方式。
handleRequest()
內部 ,第一步是驗證請求的來源。這是確保不只是任何人都可以創建 iframe、指向此文件並獲取所有 localStorage
的關鍵步驟 信息。 event
對象包含一個名為 origin
的屬性 指定發起請求的方案、域和(可選)端口(例如,“http://www.example.com”);此屬性不包含任何路徑或查詢字符串信息。 verifyOrigin()
函數只是檢查域的白名單,以確保 origin 屬性指示列入白名單的域。它通過使用正則表達式剝離協議和端口,然後在匹配 whitelist
中的域之前規範化為小寫來實現 數組。
如果來源經過驗證,則 event.data
屬性被解析為 JSON 對象和 key
屬性用作從 localStorage
讀取的鍵 .然後將一條消息作為 JSON 對象發回,其中包含最初傳遞的唯一 ID、鍵名和值;這是使用 postMessage()
完成的 在 event.source
,它是 window
的代理 發送請求的對象。第一個參數是包含來自 localStorage
的值的 JSON 序列化消息 第二個是消息應該傳遞到的來源。儘管第二個參數是可選的,但最好將目標來源作為防禦跨站點腳本 (XSS) 攻擊的額外措施。在這種情況下,原始原點被傳遞。
對於想要從 iframe 讀取數據的頁面,您需要創建 iframe 服務器並處理消息傳遞。下面的構造函數創建一個對象來管理這個過程:
/*
* Copyright 2010 Nicholas C. Zakas. All rights reserved.
* BSD Licensed.
*/
function CrossDomainStorage(origin, path){
this.origin = origin;
this.path = path;
this._iframe = null;
this._iframeReady = false;
this._queue = [];
this._requests = {};
this._id = 0;
}
CrossDomainStorage.prototype = {
//restore constructor
constructor: CrossDomainStorage,
//public interface methods
init: function(){
var that = this;
if (!this._iframe){
if (window.postMessage && window.JSON && window.localStorage){
this._iframe = document.createElement("iframe");
this._iframe.style.cssText = "position:absolute;width:1px;height:1px;left:-9999px;";
document.body.appendChild(this._iframe);
if (window.addEventListener){
this._iframe.addEventListener("load", function(){ that._iframeLoaded(); }, false);
window.addEventListener("message", function(event){ that._handleMessage(event); }, false);
} else if (this._iframe.attachEvent){
this._iframe.attachEvent("onload", function(){ that._iframeLoaded(); }, false);
window.attachEvent("onmessage", function(event){ that._handleMessage(event); });
}
} else {
throw new Error("Unsupported browser.");
}
}
this._iframe.src = this.origin + this.path;
},
requestValue: function(key, callback){
var request = {
key: key,
id: ++this._id
},
data = {
request: request,
callback: callback
};
if (this._iframeReady){
this._sendRequest(data);
} else {
this._queue.push(data);
}
if (!this._iframe){
this.init();
}
},
//private methods
_sendRequest: function(data){
this._requests[data.request.id] = data;
this._iframe.contentWindow.postMessage(JSON.stringify(data.request), this.origin);
},
_iframeLoaded: function(){
this._iframeReady = true;
if (this._queue.length){
for (var i=0, len=this._queue.length; i < len; i++){
this._sendRequest(this._queue[i]);
}
this._queue = [];
}
},
_handleMessage: function(event){
if (event.origin == this.origin){
var data = JSON.parse(event.data);
this._requests[data.id].callback(data.key, data.value);
delete this._requests[data.id];
}
}
};
CrossDomainStorage
type 封裝了通過 iframe 從不同域請求值的所有功能(請注意,它不支持保存值,這是一種非常不同的安全方案)。構造函數採用原點和路徑,它們一起用於構造 iframe 的 URL。 _iframe
_iframeReady
時屬性將保存對 iframe 的引用 表示 iframe 已完全加載。 _queue
屬性是在 iframe 準備好之前可能排隊的請求數組。 _requests
屬性存儲正在進行的請求和 _id
的元數據 是將創建唯一請求標識符的種子值。
在發出任何請求之前,init()
方法必須被調用。此方法的唯一工作是設置 iframe,添加 onload
和 onmessage
事件處理程序,然後將 URL 分配給 iframe。加載 iframe 時,_iframeLoaded()
被調用並且 _iframeReady
標誌設置為真。當時的_queue
檢查是否有在 iframe 準備好接收它們之前發出的任何請求。隊列被清空,再次發送每個請求。
requestValue()
方法是檢索值的公共 API 方法,它接受兩個參數:返回的鍵和值可用時調用的回調函數。該方法創建一個請求對像以及一個數據對象來存儲有關請求的元數據。如果 iframe 準備好,則將請求發送到 iframe,否則將元數據存儲在隊列中。 _sendRequest()
然後方法負責使用 postMesage()
發送請求。請注意,請求對像在發送之前必須序列化為 JSON,因為 postMessage()
只接受字符串。
當收到來自 iframe 的消息時,_handleMessage()
方法被調用。此方法驗證消息的來源,然後檢索消息的元數據(服務器 iframe 傳回相同的唯一標識符)以執行關聯的回調。然後清除元數據。
CrossDomainStorage
的基本用法 類型如下:
var remoteStorage = new CrossDomainStorage("http://www.example.com", "/util/server.htm");
remoteStorage.requestValue("keyname", function(key, value){
alert("The value for '" + key + "' is '" + value + "'");
});
請記住,這種技術不僅適用於不同的子域,也適用於不同的域。
實用主義
我喜歡 XAuth 的另一件事是它的實用性編寫方式:Meebo 沒有在所有瀏覽器中都提供完整的功能,而是選擇只針對功能最強大的瀏覽器。本質上,瀏覽器必須支持跨文檔消息傳遞,localStorage
,以及本機 JSON 序列化/解析,以便使用該庫。通過做出這種簡化假設,他們在製作這個實用程序時節省了大量時間和精力(可能還有很多代碼)。結果是一個非常緊湊、佔用空間小的實用程序,幾乎沒有出現重大錯誤的機會。我真的很想為作者的這種實用主義鼓掌,因為我相信這將有助於快速採用和易於進行持續維護。
諷刺的旁注
誰知道跨域客戶端數據存儲會有用?實際上,WHAT-WG 做到了。在 Web Storage 規範的初稿(當時是 HTML5 的一部分)中,有一個對象叫做 globalStorage
這允許您指定哪些域可以訪問某些數據。例如:
//all domains can access this
globalStorage["*"].setItem("foo", "bar");
//only subdomains of example.com can access this
globalStorage["*.example.com"].setItem("foo", "bar");
//only www.example.com can access this
globalStorage["www.example.com"].setItem("foo", "bar");
globalStorage
由於規範仍在不斷發展,因此在 Firefox 2 中過早地實現了接口。出於安全考慮,globalStorage
已從規範中刪除並替換為特定於來源的 localStorage
.
結論
使用 iframe 訪問另一個域的 localStorage
的基本技術 object 非常出色且適用性遠遠超出了 XAuth 用例。通過允許任何域訪問存儲在另一個域中的數據,以及基於來源的白名單,Web 開發人員現在可以在許多不同的站點之間共享數據。所有支持 localStorage
的瀏覽器 還支持原生 JSON 解析和跨文檔消息傳遞,使跨瀏覽器兼容性更加容易。 XAuth 和本文中的代碼適用於 Internet Explorer 8+、Firefox 3.5+、Safari 4+、Chrome 4+ 和 Opera 10.5+。