在Java Web應用程序中使用OpenID身份驗證
OpenID 是一套分散式身份驗證系統。通過 OpenID 我可以證明自己擁有類似 http://openid.jstevenperry.com/steve 這樣的 URL,而且可以使用經驗證的身份登錄任何支持 OpenID 的站點 — 比如 Google、Slashdot 或 Wordpress。OpenID 對終端用戶來說無疑是個不錯的工具。但是對 OpenID 的使用引發我產生這樣的想法:“如果使用 OpenID 為我給客戶編寫的基於 Java 的 Web 應用程序創建標准可靠的身份識別系統,會怎麼樣呢?”
在這個由兩部分組成的文章中,我將向您展示如何使用 openid4java 庫和知名的 OpenID 提供者 myOpenID 為基於 Java 的 Web 應用程序創建身份驗證系統。還將向您展示如何使用一個 OpenID 簡單注冊擴展(Simple Registration Extension)(SReg)接收用戶信息。
首先我將解釋什麼是 OpenID 並說明如何獲得自己的 OpenID。接下來,簡短地介紹 OpenID 身份驗證的運作方式。最後,概述使用 openid4java 執行 OpenID 身份驗證所需的步驟。在本文第 2 部分,您將了解如何創建自己的 OpenID 提供者。
我將通篇使用基於 Wicket 的 Java Web 應用程序,這是我專門為本文編寫的。您可以隨時下載應用程序 源代碼。另外,您可能希望看一下 openid4java 庫。
注意:本文重點介紹面向 Java Web 應用程序的 OpenID,不過 OpenID 在任何軟件架構模式中都有效。
OpenID 簡介
OpenID 是證明用戶擁有標識符的一種規范。現在,僅將標識符 看作惟一標識用戶的 String。如果您像我一樣,會擁有很多標識符或用戶名。我在 Facebook、Twitter 和因特網上的大量其他站點上都有用戶名。我經常嘗試使用同一個用戶名,但是這在我要注冊的每個新站點上都不可行。因此,我需要記住所有的用戶名及其對應的 Web 站點。這是一件很痛苦的事;我常常會用到 “忘記密碼?” 這一提示信息。如果有一種方法可以在所有站點使用同一個標識符,該有多好!
OpenID 恰恰可以解決這個問題。通過 OpenID,我可以聲明一個標識符,然後在采用 OpenID 協議的任意 Web 站點上使用它。最新統計(來自 OpenID Web 站點)顯示有 50,000 多個網站支持 OpenID,包括 Facebook、Yahoo!、Google 和 Twitter。
OpenID 身份驗證
OpenID 身份驗證是 OpenID 的核心,它包括三個主要概念:
OpenID 標識符:一個惟一標識用戶的文本字符串。
OpenID 依賴方(RP):一種在線資源(可能是一個 Web 站點,也可以是文件、圖像或想要進行訪問控制的任何資源),使用 OpenID 識別可以訪問它的對象。
OpenID 提供者(OP):一個站點,用戶可在該站點聲明 OpenID,隨後登錄並為任意 RP 驗證身份。
OpenID 基金會 是一個社團,該社團成員關注通過 OpenID 規范推進開源身份管理。
OpenID 如何運作?
假設有用戶嘗試訪問屬於 RP Web 站點的資源,且 RP 使用 OpenID。要訪問該資源,用戶必須以一種能被識別(規范化)為 OpenID 的形式呈現其 OpenID。OpenID 由 OP 的位置編碼。然後 RP 采用用戶標識符並將用戶重定向到 OP,此時 OP 會要求用戶證明其 ID 請求。
接下來簡要介紹一下 OpenID 規范的每個組成部分及其作用。
OpenID 標識符
OpenID 的核心部分當然是 OpenID 標識符。OpenID 標識符(或簡稱 “標識符”)是惟一標識用戶的可讀字符串。沒有兩個用戶擁有相同的 OpenID,這正是 OpenID 發揮作用的關鍵之處。通過遵循 OpenID 驗證規范 2.0 版 的規定,OpenID 依賴方能夠解碼(或 “規范化”)標識符以弄清如何驗證用戶身份。在 OpenID 的運作過程中,作為編寫代碼的開發人員,我們感興趣的是下面兩個標識符:
用戶提供的標識符
聲明的標識符
顧名思義,用戶提供的標識符是由用戶提供給 RP 的標識符。用戶提供的標識符必須被規范化 為聲明的標識符,這只是將用戶提供的標識符轉化為標准形式的一種別出心裁的說法。然後可使用聲明的標識符通過一個名為 discovery 的進程定位 OP,之後 OP 驗證該用戶身份。
OpenID 依賴方(RP)
RP 通常由用戶提供的標識符呈現,該標識符被規范化為聲明的標識符。用戶的浏覽器(“用戶代理”)將被重定向到 OP,這樣用戶便可以提供其密碼並得到身份驗證。
RP 不知道也不關心聲明的標識符是如何獲得驗證的;它只想知道 OP 是否成功地驗證了用戶身份。如果驗證成功,用戶代理(也可能是用戶的浏覽器)會被轉發到用戶正試圖訪問的安全資源中。如果用戶得不到驗證,RP 會拒絕任何訪問。
Open ID 提供者(OP)
OP(OpenID 提供者)負責發出標識符並執行用戶身份驗證。OP 還提供基於 Web 的 OpenID 管理。OP 收集並保留每個用戶的以下基本信息:
電子郵箱
全名
出生日期
郵編
國家
第一語言
當要求 OP 驗證聲明的標識符時,用戶的浏覽器直接轉到登錄頁面,用戶在該頁面輸入其密碼。此時的控制權在於 OP。如果用戶成功得到身份驗證,OP 會將浏覽器轉到 RP 指定的位置(在一個特殊的 “return-to” URL 中)。如果用戶不能進行身份驗證,他可能會收到來自 OP 的消息,指出身份驗證失敗(至少對於兩個流行的 OpenID 提供者 ClaimID 和 myOpenID 來說是這樣的)。
成為 OpenID 依賴方
現在我們了解了 OpenID 的主要組成部分,以及它們之間的協作方式。文章的其余部分將重點介紹如何使用開源 openid4java 庫編寫 OpenID 依賴方(RP)。
使用 OpenID 的第一步就是獲取一個標識符。這很簡單:只需轉到 myOpenID 並單擊 SIGN UP FOR AN OPENID 按鈕即可。選擇一個 OpenID,比如 redneckyogi 或 jstevenperry(順便提一下,兩個都是我的用戶名)。登錄窗體會告訴您所選用戶名是否已存在。如果不存在,系統將指導您輸入密碼、電子郵箱,並在 JChaptcha 格式的文本框中輸入一些文本(您不是一個機器人程序,對吧?)。
稍後,您會收到一封電子郵件,其中含有一個鏈接。單擊鏈接確認電子郵箱,然後 — 恭喜您!— 您現在擁有自己的 OpenID 了!
當然,隨著技術的不斷發展,會有更多的 OPenID 提供者可供選擇。
為表明獲取一個 OpenID 有多麼簡單快捷,我在大約 30 分鐘內用 myOpenID、Verisign 和 ClaimID 的帳戶進行了登錄。這個時間段也包括輸入詳細信息和上傳圖片所花費的時間。
您可能已經擁有 OpenID
據 OpenId.net統計,Google,Wordpress 和其他流行站點均支持 OpenID。如果您已經在這些站點上注冊,那麼您可能已經擁有一個 OpenID 了。
例如,如果您有一個 Yahoo! 帳戶,但是還希望有一個 OpenID(我就是這樣,我之前甚至不知道OpenID 是什麼)。登錄時您只需使用 Yahoo! ID 即可,Yahoo 是您的 OpenID 提供者。您使用 [email protected] 提供基於 Yahoo 的 OpenID,然後 RP 會要求 Yahoo 對您進行身份驗證(如果您運行本文附帶的示例應用程序,您實際上可以看到這個過程)。
關於示例應用程序
正如我在文章開始所講的,我使用 openid4java 編寫了 Java Web 應用程序來創建簡單的 OpenID 依賴方(RP)。這是個簡單的應用程序,您可以構建該應用程序(WAR 形式),將其放入 Tomcat,然後從本地機器上運行。示例應用程序集中關注以下幾步:
用戶在注冊頁面輸入其 OpenID。
應用程序驗證標識符(將用戶定向到其 OP 以進行登錄)
身份驗證成功之後,應用程序從 OP 獲取用戶的個人資料,然後將用戶定向到 Save 頁面,用戶可在此頁面審查並保存其個人信息。
Save 頁面上顯示的信息來自 OP。
我使用 Wicket 編寫了應用程序,是因為我真的很喜歡 Wicket。我試著盡量減少 Wicket 的 “footprint”,這樣在學習編寫 OpenID 依賴方時才不易受到擾亂。
示例應用程序的架構分為兩個職責范圍:
在 Wicket 中編寫的用戶界面
OpenID 身份驗證 — 使用 openid4java 庫
當然這兩個方面彼此交互,不過我再次嘗試減少重復部分使其更易於遵循 OpenID 規范,而不是因 Wicket 的細小部分而受到擾亂。
關於 openid4java 和示例應用程序代碼
OpenID 驗證規范 很復雜。如果您一直實現規范,您可能在編寫自己的實現時覺得很容易。不過我很懶。我不想做工作要求以外的工作以解決手頭的問題,這正是 openid4java 發揮作用的地方。openid4java 是 OpenID 驗證 規范的一個實現,它使得在編程中使用 OpenID 更簡單。
接下來的代碼顯示 openid4java API 如何調用 RP 以使用 OpenID。您可能會注意到,示例應用程序實際上需要很少的代碼來實現這個調用。openid4java 確實簡化了您的生活。
為減少示例應用程序中的 Wicket footprint,我分離出一段代碼,這段代碼將 openid4java 調用到自己的 Java 類內,這個 Java 類稱作 RegistrationService(位於 com.makotogroup.sample.model)。針對 openid4java API 的使用,該類包括 5 種方法:
getReturnToUrl() 在身份驗證成功之後返回浏覽器指向的 URL。
getConsumerManager() 用於獲取主 openid4java API 類的實例。該類處理示例 RP 應用程序執行身份驗證所需的所有代碼。
performDiscoveryOnUserSuppliedIdentifier() 顧名思義,它處理 discovery 進程中出現的潛在問題。
createOpenIdAuthRequest() 創建身份驗證所需的 AuthRequest 構造。
processReturn() 用於處理身份驗證請求的結果。
編寫 RP
身份驗證的目的是要用戶證明其身份。這樣做可以保護 Web 資源,使其免受惡意訪問者的攻擊。用戶證明了其身份之後,您決定是否要授予其訪問資源的權利(不過身份驗證不是本文的介紹范圍)。
本文的示例應用程序執行一個許多 Web 站點都常用的功能:用戶注冊。它假定用戶能證明其身份從而可以進行注冊。這是個簡單的前提,不過它表明了與 OP 的典型 “對話” 是如何進行的,且如何使用 openid4java 實現該對話。下面是一些基本步驟:
獲取用戶提供的標識符:RP 獲得用戶的 OpenID。
發現:RP 規范化用戶提供的標識符,以決定聯系哪個 OP 進行身份驗證,如何與其聯系。
關聯:並非必要步驟,不過是我強烈推薦的一步,在該步中,RP 和 OP 建立一個安全通信渠道。
身份驗證請求:RP 要求 OP 對用戶進行身份驗證。
驗證:RP 向 OP 請求用戶名驗證,並確保通信沒有受到干擾。
轉到應用程序:身份驗證之後,RP 為用戶指向其先前請求的資源。
接下來,我們將詳細分析這些步驟中的每一步,包括代碼例子。在我們逐步查看下面內容時,我將從頭到尾使用一個例子來闡述 OpenID 身份驗證過程。
獲取用戶提供的標識符
這是 RP 應用程序的任務。在工作示例中,用戶名是在應用程序的 OpenIdRegistrationPage 上獲取的。我輸入我的 OpenID 並單擊 Confirm OpenID 按鈕。示例應用程序(充當 RP)現在知道我的用戶提供標識符了。圖 1 顯示了運行中的示例應用程序的一幅截圖。
圖 1. 獲取用戶提供的標識符
在本例中,用戶提供的標識符是 redneckyogi.myopenid.com。
UI 代碼負責兩項工作:確保用戶在 Your OpenID 文本框中輸入了文本,且在用戶單擊 Confirm OpenID 按鈕時提交窗體。在確認之後,應用程序開始調用序列。清單 1 顯示了 OpenIdRegistrationPage 中提交窗格和執行調用序列所用的代碼。
清單 1. 使用 RegistrationService.java 執行 OpenID 身份驗證調用序列的 Wicket UI 代碼
Button confirmOpenIdButton = new Button("confirmOpenIdButton") {
public void onSubmit() {
String userSuppliedIdentifier = formModel.getOpenId();
DiscoveryInformation discoveryInformation =
RegistrationService.
performDiscoveryOnUserSuppliedIdentifier(
userSuppliedIdentifier);
MakotoOpenIdAwareSession session =
(MakotoOpenIdAwareSession)owningPage.getSession();
session.setDiscoveryInformation(discoveryInformation, true);
AuthRequest authRequest =
RegistrationService.createOpenIdAuthRequest(
discoveryInformation, returnToUrl);
getRequestCycle().setRedirect(false);
getResponse().redirect(authRequest.getDestinationUrl(true));
}
};
試著不要受示例及其使用 Wicket UI 代碼的方式困擾(不過如果您很好奇,完全可以查看 OpenIdRegistrationPage.java,也就是清單 1 的來源)。這裡的重點是,當用戶單擊按鈕時,UI 代碼委托 RegistrationService 的各種方法來調用 openid4java 的 API,主要做三項工作(每一項都在清單 1 中用粗體表示):
在用戶提供的標識符上執行發現
創建用於生成身份驗證請求的 openid4java AuthRequest 對象
重定向浏覽器到 OpenID 提供者
重定向浏覽器之後,UI 代碼完成任務,現在控制權在 OP 手中。注意,myopenid.com 是標識符的一部分,且用戶提供的標識符不是結構良好的 URL。在標識符中仍然需要編碼足夠的信息,以允許 openid4java 規范化並執行發現。這將在下一部分介紹。
發現(discovery)
RP 采用用戶提供的標識符,並將其轉化為一種格式,可用於確定兩個內容:OpenID 提供者(OP)是誰,如何聯系 OP。
RP 使用發現過程來確定如何向 OP 發出請求,而關鍵便是用戶提供的標識符。但是,在將用戶提供的標識符用於發現之前,首先必須將其規范化。 openid4java 實際上已經承擔了規范化用戶提供標識符的工作,所以這裡無需再作詳細討論。
兩種不同的形式是:
XRI:可擴展資源標識符
URL:統一資源定位符
本文中我們將看一些 URL 示例。圖 1 中的用戶提供標識符是一個缺少模式的 URL,因此,作為規范化工作的一部分,openid4java 向其附加 “http://”,從而構成聲明的標識符 http://redneckyogi.myopenid.com。
聲明的標識符中的編碼信息包含 OP 的名稱,在本例中是 myOpenID。由於聲明的標識符是一個 URL,openid4java 知道如何聯系 OP — 在 http://myopenid.com上 — 這正是它所要做的。
清單 2(來自示例應用程序的 RegistrationService 類)顯示 RP 如何使用 openid4java 執行發現。
清單 2. 使用 openid4java 執行發現
public static
DiscoveryInformation performDiscoveryOnUserSuppliedIdentifier(
String userSuppliedIdentifier) {
DiscoveryInformation ret = null;
ConsumerManager consumerManager = getConsumerManager();
try {
// Perform discover on the User-Supplied Identifier
List<DiscoveryInformation> discoveries =
consumerManager.discover(userSuppliedIdentifier);
// Pass the discoveries to the associate() method...
ret = consumerManager.associate(discoveries);
} catch (DiscoveryException e) {
String message = "Error occurred during discovery!";
log.error(message, e);
throw new RuntimeException(message, e);
}
return ret;
}
openid4java 進行 OpenID 身份驗證所用的核心類是 ConsumerManager。openid4java 對於該類的使用有嚴格的准則。它將該類作為靜態類成員存儲並通過 getConsumerManager() 方法予以訪問(參見示例應用程序中的 RegistrationService.java 了解更多信息)。
openid4java 允許使用一行代碼(清單 2 中粗體部分)規范化用戶提供的標識符並執行發現。返回的是 DiscoveryInformation 對象的 java.util.List。可將這些對象看作不透明對象。一定要保留這些對象,因為當您的 RP 實現選擇構建與 OP 的關聯時,要用到它們(如示例應用程序)。
關聯
關聯是 RP 和 OP 建立共享密鑰(通過 Diffie-Hellman 密鑰交換)的一種方式,能使它們之間的交互更安全可信。關聯不是 OpenID 規范所必需的。關聯是從 RP 代碼中執行的,僅需調用 ConsumerManager 上的 associate() 方法即可,如清單 3 所示。
清單 3. 使用 openid4java 建立關聯
public static
DiscoveryInformation performDiscoveryOnUserSuppliedIdentifier(
String userSuppliedIdentifier) {
DiscoveryInformation ret = null;
ConsumerManager consumerManager = getConsumerManager();
try {
// Perform discover on the User-Supplied Identifier
List<DiscoveryInformation> discoveries =
consumerManager.discover(userSuppliedIdentifier);
// Pass the discoveries to the associate() method...
ret = consumerManager.associate(discoveries);
} catch (DiscoveryException e) {
String message = "Error occurred during discovery!";
log.error(message, e);
throw new RuntimeException(message, e);
}
return ret;
}
這種方法返回 DiscoveryInformation 對象,它用來描述發現的結果(您可將該對象看作不透明對象)。示例應用程序存儲一個 session 中的 DiscoveryInformation 對象,因為稍後會用到該對象。要發出身份驗證請求,就需要該對象,接下來我們將對此進行討論。
身份驗證
RP 在用戶提供的標識符上成功執行發現後,該到驗證用戶身份的時候了。ConsumerManager 需要建立一個稱作 AuthRequest 的特殊對象,OP 會使用該對象處理身份驗證請求。
在此次交互中,需要利用名為 SimpleRegistration(簡稱 SReg)的一個 OpenID 擴展;該擴展允許 RP 提出以下請求:在響應中返回 OP 用戶資料中的某些屬性。清單 4 顯示了建立 AuthRequest 對象和使用 SReg 請求屬性的代碼。
清單 4. 建立 AuthRequest 並使用 SReg 擴展
public static AuthRequest
createOpenIdAuthRequest(DiscoveryInformation
discoveryInformation, String returnToUrl) {
AuthRequest ret = null;
//
try {
// Create the AuthRequest object
ret =
getConsumerManager().authenticate(discoveryInformation,
returnToUrl);
// Create the Simple Registration Request
SRegRequest sRegRequest =
SRegRequest.createFetchRequest();
sRegRequest.addAttribute("email", false);
sRegRequest.addAttribute("fullname", false);
sRegRequest.addAttribute("dob", false);
sRegRequest.addAttribute("postcode", false);
ret.addExtension(sRegRequest);
} catch (Exception e) {
String message = "Exception occurred while building " +
"AuthRequest object!";
log.error(message, e);
throw new RuntimeException(message, e);
}
return ret;
}
清單 4 中第一行粗體代碼顯示了對 ConsumerManager.authenticate() 的調用,它其實不執行身份驗證調用。它僅接受成功完成與 OP 的發現交互之後返回的 DiscoveryInformation 對象(參見 清單 3),以及身份驗證成功之後用戶代理(浏覽器)指向的 URL。
第二行粗體代碼顯示了如何通過對 SRegRequest.createFetchRequest() 的靜態方法調用創建 SReg 請求。然後通過對 SRegRequest 對象上 addAttribute() 的調用, 您需要的屬性作為簡單注冊擴展(Simple Registration Extension)的一部分從 OP 返回。最後,通過調用 addExtension() 將擴展添加到 AuthRequest 。
openid4java 使所有這些動作都很直觀。此時,浏覽器指向負責驗證用戶身份的 OpenID 提供者,用戶將在此頁面輸入其密碼。參見 OpenIdRegistrationPage.java 查看執行重定向的 Wicket UI 代碼。 圖 2 顯示了處理身份驗證請求的 myOpenID 服務器截圖。
圖 2. 處理身份驗證請求的 myOpenID
此時,您需要確保有代碼能處理運行於 URL 上的請求,該 URL 被指定為 “return-to” URL(參見 清單 4)。示例應用程序的 return-to URL 在 RegistrationService.getReturnToUrl() 中被硬編碼。OpenIdRegistrationSavePage 的構造函數破解 Web 請求以查明它是否從 OP 返回。如果該請求確實是從 OP 返回,它必須得到驗證。
驗證
清單 5 顯示的代碼用於查明一個請求是否來自 OP。如果是,將會有一個參數 is_return,該參數的值為 true。 如果情況是這樣的,那麼 openid4java 用於驗證請求(實際上是來自 OP 的響應)並取出 清單 4 中請求的屬性。
清單 5. 處理 return-to URL
public OpenIdRegistrationSavePage(PageParameters pageParameters) {
RegistrationModel registrationModel = new RegistrationModel();
if (!pageParameters.isEmpty()) {
String isReturn = pageParameters.getString("is_return");
if (isReturn.equals("true")) {
MakotoOpenIdAwareSession session =
MakotoOpenIdAwareSession)getSession();
DiscoveryInformation discoveryInformation =
session.getDiscoveryInformation();
registrationModel =
RegistrationService.processReturn(discoveryInformation,
pageParameters,
RegistrationService.getReturnToUrl());
if (registrationModel == null) {
error("Open ID Confirmation Failed.");
}
}
}
add(new OpenIdRegistrationInformationDisplayForm("form",
registrationModel));
}
在這段代碼中,Wicket 頁面的構造函數首先確定請求來自於 OP,是對先前身份驗證請求的響應。它使用一種定制的 Session 類(MakotoOpenIdAwareSession)抓取 DiscoveryInformation 對象,在成功完成與 OP 的發現交互之後,該對象被存儲。請求由 RegistrationService.processReturn() 方法使用 DiscoveryInformation 對象、請求參數和 return-to URL 得到驗證。如果請求驗證成功,會返回一個完全填充的 RegistrationModel 對象。這可以充當 OpenIdRegistrationSavePage 的 Wicket 模型,應用程序可在此繼續其預定作用。
轉到應用程序
如果對身份驗證的響應得到成功檢驗,用戶就有權通過 OpenID 訪問由 RP 保護的任何資源。在示例應用程序中,這是注冊過程。如果身份驗證成功,會跳出一個頁面,用戶可在此頁面審查來自 OP 的信息,並按需更改和保存信息。示例應用程序不包含真正保存注冊信息的代碼,不過有 hook。圖 3 顯示了我運行示例應用程序驗證我的 OpenID 時來自 OP 的信息。
Figure 3. 顯示來自 OP 的個人資料信息的示例應用程序
結束語
OpenID 用於解決大量的在線身份驗證問題,已經作為一種可靠的身份管理解決方案而被廣為接受。OpenID 的獲取很簡單,目前注冊的 OpenID 已經達到數百萬個。與任何其他規范一樣,OpenID 身份驗證 很復雜,不過 openid4java 極大地簡化了它。在本文中,您已經看到了 OpenID 身份驗證的運作方式。您也了解了使用 openid4java 將 OpenID 加入 Java Web 應用程序中有多麼簡單。
在本文第 2 部分,我們將著重介紹 OpenID 謎題的另外半部分:編寫 OpenID 提供者。這一部分的討論也是圍繞示例代碼展開的,使用專門為本文編寫的示例 Java Web 應用程序。同時,為在 Java Web 應用程序中實現 OpenID 身份驗證,請隨意使用 RegistrationService.java 上的代碼。
下載
描述 名字 大小 下載方法 OpenID 示例 openid4java-sample-app.zip 4.3 MB HTTP