面向 Java Web 应用程序的 OpenID,第 1 部分:在 Java Web 应用程序中使用 OpenID 身份验证
2010-03-25 00:00:00 来源:WEB开发网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 提供者。您使用 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 实现该对话。下面是一些基本步骤:
获取用户提供的标识符: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 |
更多精彩
赞助商链接