当前位置: 代码迷 >> Web前端 >> 面临 Java Web 应用程序的 OpenID
  详细解决方案

面临 Java Web 应用程序的 OpenID

热度:653   发布时间:2012-10-09 10:21:45.0
面向 Java Web 应用程序的 OpenID

第 1 部分 在 Java Web 应用程序中使用 OpenID 身份验证转载自

简介:?OpenID 是一个分散式身份识别协议,能使用户更易于访问 Java? Web 应用程序中的资源。 在由两部分组成的文章的第 1 部分中,您将了解 OpenID 验证规范 以及在 Java 示例应用程序中加入该规范的步骤。作者 J. Steven Perry 没有手动实现 OpenID 验证规范,而是使用 openid4java 库和一个流行的 OpenID 提供者 myOpenID,为 Wicket 中编写的 Java 应用程序创建安全可靠的注册过程。

?

--------------===============================---------

?

内容

  • OpenID 简介
  • OpenID 身份验证
  • OpenID 如何运作?
  • 成为 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,比如 redneckyogijstevenperry(顺便提一下,两个都是我的用户名)。登录窗体会告诉您所选用户名是否已存在。如果不存在,系统将指导您输入密码、电子邮箱,并在 JChaptcha 格式的文本框中输入一些文本(您不是一个机器人程序,对吧?)。

稍后,您会收到一封电子邮件,其中含有一个链接。单击链接确认电子邮箱,然后 ― 恭喜您!― 您现在拥有自己的 OpenID 了!

当然,随着技术的不断发展,会有更多的 OPenID 提供者可供选择(参见 参考资料 获取完整列表)。

为表明获取一个 OpenID 有多么简单快捷,我在大约 30 分钟内用 myOpenID、Verisign 和 ClaimID 的帐户进行了登录。这个时间段也包括输入详细信息和上传图片所花费的时间。

您可能已经拥有 OpenID

据 OpenId.net统计,Google,Wordpress 和其他流行站点均支持 OpenID。如果您已经在这些站点上注册,那么您可能已经拥有一个 OpenID 了。

例如,如果您有一个 Yahoo! 帐户,但是还希望有一个 OpenID(我就是这样,我之前甚至不知道OpenID 是什么)。登录时您只需使用 Yahoo! ID 即可,Yahoo 是您的 OpenID 提供者。您使用 whatever@yahoo.com 提供基于 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 实现该对话。下面是一些基本步骤:

  1. 获取用户提供的标识符:RP 获得用户的 OpenID。

  2. 发现:RP 规范化用户提供的标识符,以决定联系哪个 OP 进行身份验证,如何与其联系。

  3. 关联:并非必要步骤,不过是我强烈推荐的一步,在该步中,RP 和 OP 建立一个安全通信渠道。

  4. 身份验证请求:RP 要求 OP 对用户进行身份验证。

  5. 验证:RP 向 OP 请求用户名验证,并确保通信没有受到干扰。

  6. 转到应用程序:身份验证之后,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 中用粗体表示):

  1. 在用户提供的标识符上执行发现

  2. 创建用于生成身份验证请求的 openid4java AuthRequest 对象

  3. 重定向浏览器到 OpenID 提供者

重定向浏览器之后,UI 代码完成任务,现在控制权在 OP 手中。注意,myopenid.com 是标识符的一部分,且用户提供的标识符不是结构良好的 URL。在标识符中仍然需要编码足够的信息,以允许 openid4java 规范化并执行发现。这将在下一部分介绍。

发现(discovery)

RP 采用用户提供的标识符,并将其转化为一种格式,可用于确定两个内容:OpenID 提供者(OP)是谁,如何联系 OP。

RP 使用发现过程来确定如何向 OP 发出请求,而关键便是用户提供的标识符。但是,在将用户提供的标识符用于发现之前,首先必须将其规范化。 openid4java 实际上已经承担了规范化用户提供标识符的工作,所以这里无需再作详细讨论。

两种不同的形式是:

  1. XRI:可扩展资源标识符
  2. 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
处理身份验证请求的 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 已经达到数百万个。与任何其他规范一样,OpenID 身份验证 很复杂,不过 openid4java 极大地简化了它。在本文中,您已经看到了 OpenID 身份验证的运作方式。您也了解了使用 openid4java 将 OpenID 加入 Java Web 应用程序中有多么简单。

在本文第 2 部分,我们将着重介绍 OpenID 谜题的另外半部分:编写 OpenID 提供者。这一部分的讨论也是围绕示例代码展开的,使用专门为本文编写的示例 Java Web 应用程序。同时,为在 Java Web 应用程序中实现 OpenID 身份验证,请随意使用 RegistrationService.java 上的代码。

<!-- CMA ID: 476423 --><!-- Site ID: 10 --><!-- XSLT stylesheet used to transform this file: dw-article-6.0-beta.xsl -->


下载

描述 名字 大小 下载方法
OpenID 示例 openid4java-sample-app.zip 4.3 MB HTTP

?

第 2 部分: 为单点登录身份验证编写 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 请求。

关于 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 请求

</table
  相关解决方案
				

  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"));
    . . 
  }