分享

面向 Java Web 应用程序的 OpenID

 风_宇星 2015-02-28

OpenID 是一套分散式身份验证系统。通过 OpenID 我可以证明自己拥有类似 http://openid./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 是证明用户拥有标识符的一种规范。现在,仅将标识符 看作惟一标识用户的 String。 如果您像我一样,会拥有很多标识符或用户名。我在 Facebook、Twitter 和因特网上的大量其他站点上都有用户名。我经常尝试使用同一个用户名,但是这在我要注册的每个新站点上都不可行。因此,我需要记住所有的用户名及其对应的 Web 站点。这是一件很痛苦的事;我常常会用到 “忘记密码?” 这一提示信息。如果有一种方法可以在所有站点使用同一个标识符,该有多好!

OpenID 恰恰可以解决这个问题。通过 OpenID,我可以声明一个标识符,然后在采用 OpenID 协议的任意 Web 站点上使用它。最新统计(来自 OpenID Web 站点)显示有 50,000 多个网站支持 OpenID,包括 Facebook、Yahoo!、Google 和 Twitter。

OpenID 身份验证是 OpenID 的核心,它包括三个主要概念:

  • OpenID 标识符:一个惟一标识用户的文本字符串。

  • OpenID 依赖方(RP):一种在线资源(可能是一个 Web 站点,也可以是文件、图像或想要进行访问控制的任何资源),使用 OpenID 识别可以访问它的对象。

  • OpenID 提供者(OP):一个站点,用户可在该站点声明 OpenID,随后登录并为任意 RP 验证身份。

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 的主要组成部分,以及它们之间的协作方式。文章的其余部分将重点介绍如何使用开源 openid4java 库编写 OpenID 依赖方(RP)。

使用 OpenID 的第一步就是获取一个标识符。这很简单:只需转到 myOpenID 并单击 SIGN UP FOR AN OPENID 按钮即可。选择一个 OpenID,比如 redneckyogijstevenperry(顺便提一下,两个都是我的用户名)。登录窗体会告诉您所选用户名是否已存在。如果不存在,系统将指导 您输入密码、电子邮箱,并在 JChaptcha 格式的文本框中输入一些文本(您不是一个机器人程序,对吧?)。

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

当然,随着技术的不断发展,会有更多的 OPenID 提供者可供选择。

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

正如我在文章开始所讲的,我使用 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 显示了运行中的示例应用程序的一幅截图。

在本例中,用户提供的标识符是 redneckyogi.

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 手中。注意, 是标识符的一部分,且用户提供的标识符不是结构良好的 URL。在标识符中仍然需要编码足够的信息,以允许 openid4java 规范化并执行发现。这将在下一部分介绍。

发现(discovery)

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

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

两种不同的形式是:

  1. XRI:可扩展资源标识符
  2. URL:统一资源定位符

本文中我们将看一些 URL 示例。图 1 中的用户提供标识符是一个缺少模式的 URL,因此,作为规范化工作的一部分,openid4java 向其附加 “http://”,从而构成声明的标识符 http://redneckyogi.

声明的标识符中的编码信息包含 OP 的名称,在本例中是 myOpenID。由于声明的标识符是一个 URL,openid4java 知道如何联系 OP — 在 http://上 — 这正是它所要做的。

清单 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 上的代码。

此文来自 IBM developerWorks。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多