程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 面向Java Web應用程序的OpenID,第2部分

面向Java Web應用程序的OpenID,第2部分

編輯:關於JAVA

為單點登錄身份驗證編寫OpenID提供者

簡介:本文中,您將學習如何使用 OpenID 保護 Java Web 應用程序資源,以防止被沒有經過身份驗證的用戶的損害。在這個介紹 OpenID 身份驗證規范的系列的第 2 部分中,Steve Perry 講解如何使用 openid4java 庫為單點登錄場景創建 OpenID 提供者。通過在“閉環”架構中構建作為 OpenID 提供者的應用程序,可以讓最終用戶只需登錄一次,即可訪問多個應用程序。另外,您還將學習如何使用 OpenID Attribute Exchange (AX) 擴展定制 OpenID 依賴方和提供者之間的數據交換。

OpenID 是一個可靠的身份管理和身份驗證解決方案,在世界各地有許多用戶。它讓最終用戶可以使用一個得到廣泛認可的用戶 ID 訪問許多網站和其他在線資源。在 第 1 部分 中,我介紹了 OpenID 身份驗證規范,講解了如何使用 openid4java 庫實現把它集成到 Java Web 應用程序中。

第 1 部分主要關注 OpenID 依賴方 (RP),RP 是使用 OpenID 進行注冊和身份驗證的在線資源(比如網站或 MP3)。OpenID 身份驗證 規范的另一半是 OpenID 提供者 (OP)。OP 幫助用戶申請 OpenID,對用戶進行身份驗證以登錄與 OpenID 兼容的 Web 資源。

目前已經有許多 OpenID 提供者(包括 第 1 部分 中討論的 Java Web 應用程序注冊系統所用的 OP,myOpenID),在大多數情況下不需要自己創建 OP。

在一種場景中構建自己的 OP 是有意義的:應用程序集群中的多個應用程序共享可信網絡中的資源。在這種情況下,可能希望創建一個安全的 “閉環” 系統。這讓用戶可以同時登錄所有應用程序,而不必分別登錄每個應用程序,非常方便。讓集群中的一個應用程序作為 OP,就可以為所有應用程序建立單點登錄身份驗證。

在本文中,我們要在閉環架構中編寫一個 OpenID 提供者以保護許多應用程序。首先討論一下單點登錄身份驗證的好處和結構,然後為集群架構編寫一個簡單的 OpenID 提供者。我們仍然使用 openid4java 庫提供身份驗證系統的核心運行時功能,從而確保我們的 OpenID 提供者符合 OpenID 身份驗證規范。

單點登錄身份驗證

在某些企業場景中,與把所有功能構建為單一應用程序相比,把具有不同功能的應用程序組合起來更有意義。這樣的應用程序集群常常是 B2B 的核心,每個參與方都提供某些服務,以此增加整個業務體系的價值。

開發這種集群的困難在於身份驗證;讓每個應用程序分別對最終用戶進行身份驗證是不可行的,至少從最終用戶的角度來說不行。

在使用 OpenID 標准進行身份驗證的集群系統中,每個參與的應用程序都把身份驗證委托給 OP。每個應用程序確信對其功能和資源的訪問是安全的,而最終用戶在每次會話中只需登錄一次。

我們來研究一下單點登錄身份驗證系統中的參與方。注意,下面討論的架構基於 第 1 部分 中開發的示例應用程序。

OpenID 依賴方 (RP)

OpenID 依賴方 是網站或其他在線資源,它們要求對其內容的訪問是安全的。RP 使用 OpenID 提供者 (OP) 驗證用戶的身份。RP 還可以使用 Simple Registration (SReg) 和/或 Attribute Exchange (AX) 擴展注冊或識別用戶的相關信息。當請求 OP 驗證用戶的身份時,RP 通過調用 openid4java 庫發出 SReg 和 AX 請求。

OpenID 提供者 (OP)

OpenID 提供者為所有參與的應用程序提供身份驗證。通過調用 openid4java 庫成功地驗證用戶的身份之後,OP 就會滿足來自 RP 的 SReg 和 AX 請求。在本文討論的單點登錄架構中,OP 處於中心位置。

編寫 OpenID 提供者

在前一篇文章中,講解了如何使用 openid4java 為 Java Web 應用程序注冊系統編寫依賴方。在本文中,我們按相似的過程編寫 OpenID 提供者。openid4java 確保 OpenID 提供者符合 OpenID 身份驗證規范,因為所有 OpenID 基礎設施已經編寫好了。

關於示例應用程序

示例應用程序的目的是演示 OpenID RP 和 OP 如何協作以防止未授權的資源訪問。示例應用程序的流程非常明確:

用戶試圖訪問受保護的資源。

RP 請求 OP 驗證用戶的身份。

如果用戶還沒有登錄的話,OP 驗證用戶的身份。

RP 判斷登錄的用戶是否有權訪問受保護的資源。

示例應用程序包含 RP 和 OP 的代碼,這樣您可以看到它們的協作方式。在真實的場景中,不會把這兩個組件部署在同一個應用程序中 — 完全沒有理由這麼做! — 但是把它們放在一起有助於研究它們的交互方式。

示例應用程序中的代碼清單

本節中的代碼清單演示 OP(和 RP)如何通過調用 openid4java API 使用 OpenID。您可能會注意到示例應用程序實際上需要的代碼非常少。openid4java 確實大大簡化了開發。RP 使用的代碼基本上與在 第 1 部分 中看到的代碼差不多,關於 RP 內部原理的更多信息參見第 1 部分。我會指出幾處差異(主要與第 1 部分中沒有討論的 AX 相關)。

與為第 1 部分編寫的應用程序一樣,這個應用程序也使用 Wicket 作為 UI。為了減少示例應用程序中 Wicket 的內存占用量,我把 OP 用來調用 openid4java 的代碼隔離在它自己的 Java 類 OpenIdProviderService 中(在 com.makotogroup.sample.model 中)。

OpenIdProviderService.java 包含幾個方法,它們與 openid4java API 的使用方法對應:

getServerManager() 配置並返回 openid4java ServerManager 類的引用。

getOpEndpointUrl() 返回 OP 從 RP 接收請求的位置的端點 URL。

processAssociationRequest() 應 RP 的請求使用 openid4java 關聯 OP。

sendDiscoveryResponse() 把發現響應發送給 RP。

createAuthResponse() 創建在處理身份驗證請求之後發送給 RP 的 openid4java AuthResponse 消息。

buildAuthResponse() 是處理 OpenID Simple Registration 和 Attribute Exchange 請求的核心方法。

啟動示例應用程序的方法是,運行 Ant [REF] 並構建 WAR 目標,然後把它復制到 Tomcat webapps 目錄並啟動 Tomcat。

OpenID 身份驗證:步驟

當用戶試圖訪問依賴方 (RP) 的受保護資源時,RP 要確認用戶的身份是真實的(身份驗證),然後決定是否授予用戶訪問權(授權)。本文的重點是身份驗證,所以如果 OpenID 提供者 (OP) 驗證了用戶的身份,示例應用程序就會授予對受保護資源的訪問權。在真實的場景中,RP 還會執行某種授權。

在運行示例應用程序時,會看到一個包含受保護資源的屏幕。這個過程中會發生以下事件,下面幾節詳細討論這些事件:

請求訪問受保護資源:用戶試圖訪問 RP 網站上的受保護資源。

RP 執行發現:RP 向 OP 發送發現請求以建立連接和執行關聯。

OP 響應發現請求:OP 通過 SReg、Attribute Exchange (AX) 或 OpenID Provider Authentication Policy (AP) 擴展發送回一個 XRDS (eXtensible Resource Descriptor Sequence),以此響應發現請求。XRDS 確認這個 OP 是用戶的 OpenID 服務提供者。

RP 請求驗證用戶的身份:RP 向 OP 詢問是否可以驗證用戶的身份。如果登錄成功,RP 使用 SReg 和/或 AX 擴展請求某些用戶信息。

OP 驗證用戶的身份:如果用戶沒有登錄或者用戶會話無效,就要求用戶提供登錄憑證。如果身份驗證成功,OP 就通知 RP 並發送通過 SReg 和/或 AX 請求的數據。

RP 授予訪問權:授予用戶對受保護資源的訪問權。在真實的場景中,大多數 RP 會在授予訪問權之前檢查用戶的授權。

下面詳細討論每個步驟。

為什麼要使用 AX 擴展?

這個示例應用程序使用 OpenID SReg 和 AX 擴展在 OP 和 RP 之間傳遞用戶信息。這兩個擴展都讓 OP 和 RP 可以高效地通信。SReg 提供有限的可交換屬性,而 AX 實際上可以用來交換任何信息,只要 OP 和 RP 都把它定義為屬性。在集群場景中,每個可信的應用程序 (RP) 還可能定義自己的定制的 “廠商擴展”。這是改進 OP 和 RP 之間的通信的另一種方法。本文後面會進一步討論 AX 擴展。

請求訪問受保護資源

示例應用程序 包含一個受保護資源。當應用程序啟動並訪問 RP URL (http://localhost:8080/openid-provider-sample-app/) 時,裝載以下頁面:

圖 1. 示例應用程序的主頁面

當用戶單擊這個鏈接時,執行清單 1 中的代碼:

清單 1. 包含受保護資源的應用程序主頁面

package com.makotogroup.sample.wicket;
. . .
public class OleMainPage extends WebPage {
 public OleMainPage() {
  add(new OleMainForm("form"));
 }
 public class OleMainForm extends Form {
  public OleMainForm(String id) {
   super(id);
   add(new PageLink("openIdRegistrationPage", new IPageLink() {
    public Page getPage() {
     return new OpenIdRegistrationPage();
    }
    public Class<? extends WebPage> getPageIdentity() {
     return OpenIdRegistrationPage.class;
    }
   }));
  }
 }
}

請注意清單 1 中的粗體代碼。當用戶單擊圖 1 所示的鏈接時,Wicket 把用戶帶到 OpenIdRegistrationPage(資源)。這時,調用鏈接的目的地,這會運行 OpenIdRegistrationPage 類的構造器。這個類有兩個作用:

作為初始調用的入口點。

作為身份驗證成功之後從 OP “回調” 的目標。

在發出初始調用以訪問這個頁面時,沒有傳遞 Wicket PageParameters,RP 知道需要請求 OP 驗證用戶的身份。

RP 執行發現

為了在 RP 和 OP 之間通信,RP 必須對 OP 執行發現。從編程的角度來看,這很簡單(同樣是由於 openid4java 簡化了編程),但這是一個重要的步驟,所以我把代碼分解出來討論一下。

RP 使用下面的代碼(取自 OpenIdRegistrationPage 的構造器)發送發現請求:

DiscoveryInformation discoveryInformation =
  RegistrationService.performDiscoveryOnUserSuppliedIdentifier(
     OpenIdProviderService.getOpEndpointUrl());

在這段代碼中,RP 做兩件事:

對 OP 的端點 URL 執行發現。

把本身與 OP 關聯起來。(對 Diffie-Hellman 密鑰交換和關聯期間發生的其他活動的詳細解釋參見 第 1 部分。)

接下來,由 OP 處理 RP 的發現請求。

OP 響應發現請求

請記住,在示例應用程序的 RP 和 OP 端都運行 openid4java。因此,在發現 OP 的過程中,openid4java 的 RP 端向 OP 的端點 URL 發送一個空的請求。端點 URL 是聯系 OP 的位置,OP 在這裡接收所有來自 RP 的請求。OP 必須處理這個請求。看一下 OpenIdProviderService.getOpEndpointUrl(),會注意到端點 URL 是 http://localhost:8080/openid-provider-sample-app/sample/OpenIdLoginPage。

當 RP 向 OP 發送空的請求時,Wicket 構造 OpenIdLoginPage 並運行它的構造器,見清單 2:

清單 2. OP 入口點

public OpenIdLoginPage(PageParameters parameters) throws IOException {
  super(parameters);
  if (parameters.isEmpty()) {
   // Empty request. Assume discovery request...
   OpenIdProviderService.sendDiscoveryResponse (getResponse());
 . . .

注意,如果 OP 接收到空的請求,它會假設這是發現請求。然後,它創建一個 XRDS 文檔並發送回請求者。

清單 3 給出 sendDiscoveryRequest() 的代碼:

清單 3. 發送對發現請求的響應

public static void sendDiscoveryResponse (Response response) throws IOException {
  //
  response.setContentType("application/xrds+xml");
  OutputStream outputStream = response.getOutputStream();
  String xrdsResponse = OpenIdProviderService.createXrdsResponse();
  //
  outputStream.write(xrdsResponse.getBytes());
  outputStream.close();
 }

這個 XRDS 文檔對於 openid4java 的 RP 端的正確運行很重要。

當 RP 收到 OP 發送的 XRDS 文檔時,它知道它已經聯系到了這個用戶的 OP。然後,RP 創建身份驗證請求並發送給 OP。

RP 請求驗證用戶的身份

RP 請求 OP 確認是否可以驗證用戶的身份。它執行的一系列調用見清單 4(取自構造器):

清單 4. RP 代碼把身份驗證委托給 OP

DiscoveryInformation discoveryInformation =
  RegistrationService.performDiscoveryOnUserSuppliedIdentifier(
     OpenIdProviderService.getOpEndpointUrl());
 MakotoOpenIdAwareSession session =
  (MakotoOpenIdAwareSession)getSession();
 session.setDiscoveryInformation(discoveryInformation, true);
 AuthRequest authRequest =
  RegistrationService.createOpenIdAuthRequest(
     discoveryInformation,
     RegistrationService.getReturnToUrl());
 getRequestCycle().setRedirect(false);
 getResponse().redirect(authRequest.getDestinationUrl(true));

首先,RP 通過端點 URL 聯系 OP。這個調用可能看起來有點兒奇怪,但是請記住,在這個場景中應用程序集群使用一個可信的伙伴作為 OP。從 RP 的角度來看,驗證用戶提供的身份只需發現 OP 的位置,讓 openid4java 構造後續交互所需的對象。OP 負責處理身份驗證機制。

接下來,獲取當前的 Wicket Session,把從 openid4java 獲取的 DiscoveryInformation 存儲起來供以後使用。我編寫了一個特殊的 Session 子類 MakotoOpenIdAwareSession,這樣便於在 Session 中存儲 openid4java 對象。

然後,用從 openid4java 獲取的 DiscoveryInformation 對象創建身份驗證請求。這個對象告訴 Wicket 重定向到哪裡以執行身份驗證調用。

這些步驟與第 1 部分中的步驟相同。我在這裡重復解釋它們是因為本文使用的示例應用程序架構與第 1 部分的代碼不太一樣。我還希望您查看 OP 端的 API 調用,能夠把它們聯系在一起。

現在,RP 等待 OP 發送身份驗證響應。在討論下一個步驟之前,我們先看一下 Attribute Exchange 在用戶身份驗證中的作用。

OpenID Attribute Exchange 擴展

在第 1 部分中,我們簡要討論了 Simple Registration (SReg) 擴展,可以用 SReg 在 RP 和 OP 之間交換特定的信息集(由 SReg 規范定義)。看一下本文示例應用程序中的 createOpenIdAuthRequest() 方法,會注意到 RP 使用另一個擴展 OpenID Attribute Exchange (AX) 向 OP 請求信息。

與 SReg 擴展一樣,OpenID Attribute Exchange (AX) 用於在 RP 和 OP 之間以一致的標准的方式交換信息。但是與 SReg 不同,AX 允許 OpenID 依賴方和提供者交換不受限制的信息,只要 RP 和 OP 都支持 AX 擴展。

簡單地說,RP 通過消息請求 OP 提供特定的信息,OP 在消息中發送回這些信息。這些消息編碼在浏覽器重定向到的 URL 中,但是 openid4java 使用對象讓代碼可以使用這些信息。

RP 使用 FetchRequest 類發出 AX 請求。得到消息對象的引用之後,添加它希望從 OP 返回的屬性,見清單 5:

清單 5. 包含屬性的 RP FetchRequest


AuthRequest ret = obtainSomehow();
// Create AX request to get favorite color
FetchRequest fetchRequest = FetchRequest.createFetchRequest();
fetchRequest.addAttribute("favoriteColor",
    "http://makotogroup.com/schema/1.0/favoriteColor",
    false);
ret.addExtension(fetchRequest);

當 OP 把信息發送回 RP 時,使用相同的構造,見清單 6:

清單 6. OP 發送回請求的屬性


if (authRequest.hasExtension(AxMessage.OPENID_NS_AX)) {
 MessageExtension extensionRequestObject =
   authRequest.getExtension(AxMessage.OPENID_NS_AX);
 FetchResponse fetchResponse = null;
 Map<String, String> axData = new HashMap<String, String>();
 if (extensionRequestObject instanceof FetchRequest) {
  FetchRequest axRequest = (FetchRequest)extensionRequestObject;
  ParameterList parameters = axRequest.getParameters();
  fetchResponse = FetchResponse.createFetchResponse(
    axRequest, axData);
  if (parameters.hasParameter("type.favoriteColor")) {
    axData.put("favoriteColor", registrationModel.getFavoriteColor());
   fetchResponse.addAttribute("favoriteColor",
     "http://makotogroup.com/schema/1.0/favoriteColor",
     registrationModel.getFavoriteColor());
  }
   authResponse.addExtension(fetchResponse);
 } else {
  // ERROR
 }
}

定義的每個屬性有一個簡單的名稱和相關聯的 URI。在這裡,屬性的簡單名稱是 FavoriteColor,它的 URI 是 http://makotogroup.com/schema/1.0/favoriteColor。

另外,屬性必須能夠轉換為字符串 (以這種方式發送數據字段的示例見示例應用程序)。在定義要在 RP 和 OP 之間交換的屬性時,兩端對於屬性的定義必須一致;除此之外,沒有任何限制!

現在,討論下一個應用程序交互步驟。

OP 驗證用戶的身份

在上一步中,身份驗證請求已經到達了 OP 的端點 URL。接下來,OP 分解請求以決定後續操作。OP 打開請求,獲取它的模式,模式可能是關聯或身份驗證。

清單 7. OP 處理關聯請求

//From (OpenIdLoginPage's constructor): 

public OpenIdLoginPage(PageParameters parameters) throws IOException {
  super(parameters);
  . . .
  if ("associate".equals(mode)) {
    OpenIdProviderService.processAssociationRequest(getResponse(), requestParameters);
   }
  . . .
}

//From (OpenIdProviderService): 

 public static void processAssociationRequest(Response response, ParameterList request)
    throws IOException {
  Message message = getServerManager().associationResponse(request);
  sendPlainTextResponse(response, message);
 }
 private static void sendPlainTextResponse(Response response, Message message)
    throws IOException {
  response.setContentType("text/plain");
  OutputStream os = response.getOutputStream();
  os.write(message.keyValueFormEncoding().getBytes());
  os.close();
 }

在清單 7 中,OpenIdLoginPage 的構造器(示例應用程序中 OP 的入口點)首先分解請求。模式表明這是一個關聯請求,所以它把關聯機制委托給 openid4java,openid4java 的代碼包裝在 OpenIdProviderService.java 中。把關聯響應發送回 RP。

RP 確認已經建立關聯之後(實際上是 openid4java 確認之後),RP 向 OP 發送另一個調用。OP 再次分解並處理請求。大多數情況下,這是一個 checkid_authentication 請求。

清單 8 給出 OpenIdLoginPage 構造器中的代碼:

清單 8. openid4java 分解 checkid_authentication 請求

public OpenIdLoginPage(PageParameters parameters) throws IOException {
  super(parameters);
   . . .
   else if ("checkid_immediate".equals(mode)
    ||
     "checkid_setup".equals(mode)
    ||
     "check_authentication".equals(mode)) {
    if (((MakotoOpenIdAwareSession)getSession()).isLoggedIn()) {
     // Create AuthResponse from session variables...
     sendSuccessfulResponse();
    }
  add(new OpenIdLoginForm("form"));
  . .
 }

注意清單 8 中的兩行粗體代碼。在第一行粗體代碼中,OpenIdLoginPage 發送回一個成功的響應。首先,OpenIdLoginPage 使用一個 Session 對象判斷用戶是否已經登錄了(登錄的用戶應該不必再次登錄)。如果用戶已經登錄了,它返回一個成功的身份驗證消息。這就是示例應用程序中實現單點登錄的方法。

如果用戶還沒有登錄,Wicket 就創建一個登錄表單,用戶可以在其中輸入用戶憑證,見圖 2:

圖 2. OP 向未驗證身份的用戶顯示登錄屏幕

如果用戶成功地驗證身份,就發送回一個成功的響應並把一些信息(具體地說是 DiscoveryInformation 對象)存儲在 Session 中。這就是單點登錄的底層機制。

現在,把浏覽器重新定向到 RP 的 "return-to" URL 並發送成功的身份驗證響應。

RP 授予訪問權

如果用戶成功地登錄,OP 會發送回一個成功的 AuthResponse 消息。現在,由 RP 授予用戶訪問權。如果用戶通過了 OP 的身份驗證,示例應用程序會自動地授予訪問權。另外,OP 發送 RP 請求的所有用戶信息。在圖 3 中,RP 在它的注冊屏幕上顯示信息:

圖 3. 示例應用程序的注冊頁面顯示從 OP 獲取的信息

結束語

在本文中,您看到了如何使用 OpenID 身份驗證 規范為伙伴應用程序集群建立單點登錄身份驗證。如果伙伴應用程序可以相互信任,就可以以其中的一個伙伴作為 OpenID 提供者 (OP),實現單點登錄身份驗證。

使用 OpenID 進行身份驗證和數據交換可以確保所有參與的伙伴應用程序在身份驗證和授權方面保持一致。OpenID 是一個得到廣泛采用的標准,有許多資源可以幫助您學習和調試 OpenID 身份驗證 實現。

如果想詳細了解本文中實現的單點登錄架構,請研究源代碼。只需把它構建為 WAR,部署到 Tomcat,然後運行它!一定要打開 TRACE 日志記錄,查看日志輸出,日志會揭示本文中沒有討論的應用程序細節。

與任何規范一樣,OpenID 身份驗證很復雜,但是 openid4java 大大簡化了使用它的過程。

下載

描述 名字 大小 下載方法 示例應用程序的源代碼 openid-provider-sample-app.zip 4.5KB HTTP
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved