面向 Java Web 应用程序的 OpenID,第 2 部分: 为单点登录身份验证编写 OpenID 提供者

本文中,您将学习如何使用 OpenID 保护 Java Web 应用程序资源,以防止被没有经过身份验证的用户的损害。在这个介绍 OpenID 身份验证规范的系列的第 2 部分中,Steve Perry 讲解如何使用 openid4java 库为单点登录场景创建 OpenID 提供者。通过在“闭环”架构中构建作为 OpenID 提供者的应用程序,可以让最终用户只需登录一次,即可访问多个应用程序。另外,您还将学习如何使用 OpenID Attribute Exchange (AX) 扩展定制 OpenID 依赖方和提供者之间的数据交换。

J Steven Perry, 首席顾问, Makoto Consulting Group, Inc.

J Steven Perry 是一名软件开发人员、架构师和全能 Java 专家,他从 1991 年起就从事专业的软件开发。他的专业兴趣包括 JVM 的内部工作原理和 UML 建模,以及介于两者之间的所有内容。Steve 编写了从技术文档到 Java 代码等各种内容,并且对教学和培训也充满了热情。Steve 是 Java Management Extensions(O’Reilly)的作者,Java Enterprise Best Practices(O’Reilly)的合著者,并撰写了有关软件开发主题和 O'Reilly ShortCut: Log4J 的杂志文章。



2010 年 4 月 27 日

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 请求。

关于 SReg 的更多信息

第 1 部分中的示例应用程序使用 OpenID Simple Registration 扩展,让 RP 可以向 OP 请求用户信息。关于 OpenID SReg 扩展的详细信息参见 “面向 Java Web 应用程序的 OpenID,第 1 部分”。本文的应用程序将使用 OpenID Attribute Exchange 扩展,它支持更复杂的信息事务。

OpenID 提供者 (OP)

OpenID 提供者为所有参与的应用程序提供身份验证。通过调用 openid4java 库成功地验证用户的身份之后,OP 就会满足来自 RP 的 SReg 和 AX 请求。在本文讨论的单点登录架构中,OP 处于中心位置。

编写 OpenID 提供者

在前一篇文章中,讲解了如何使用 openid4java 为 Java Web 应用程序注册系统编写依赖方。在本文中,我们按相似的过程编写 OpenID 提供者。openid4java 确保 OpenID 提供者符合 OpenID 身份验证规范,因为所有 OpenID 基础设施已经编写好了。

关于示例应用程序

示例应用程序的目的是演示 OpenID RP 和 OP 如何协作以防止未授权的资源访问。示例应用程序的流程非常明确:

  1. 用户试图访问受保护的资源。
  2. RP 请求 OP 验证用户的身份。
  3. 如果用户还没有登录的话,OP 验证用户的身份。
  4. 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 还会执行某种授权。

在运行示例应用程序时,会看到一个包含受保护资源的屏幕。这个过程中会发生以下事件,下面几节详细讨论这些事件:

  1. 请求访问受保护资源:用户试图访问 RP 网站上的受保护资源。
  2. RP 执行发现:RP 向 OP 发送发现请求以建立连接和执行关联。
  3. OP 响应发现请求:OP 通过 SReg、Attribute Exchange (AX) 或 OpenID Provider Authentication Policy (AP) 扩展发送回一个 XRDS (eXtensible Resource Descriptor Sequence),以此响应发现请求(这里不讨论 AP 扩展;参见 参考资料)。XRDS 确认这个 OP 是用户的 OpenID 服务提供者。
  4. RP 请求验证用户的身份:RP 向 OP 询问是否可以验证用户的身份。如果登录成功,RP 使用 SReg 和/或 AX 扩展请求某些用户信息。
  5. OP 验证用户的身份:如果用户没有登录或者用户会话无效,就要求用户提供登录凭证。如果身份验证成功,OP 就通知 RP 并发送通过 SReg 和/或 AX 请求的数据。
  6. 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 做两件事:

  1. 对 OP 的端点 URL 执行发现。
  2. 把本身与 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 身份验证。


下载

描述名字大小
示例应用程序的源代码openid-provider-sample-app.zip4.5KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=485752
ArticleTitle=面向 Java Web 应用程序的 OpenID,第 2 部分: 为单点登录身份验证编写 OpenID 提供者
publish-date=04272010