WEB开发网
开发学院软件开发Java 面向 Java Web 应用程序的 OpenID,第 1 部分:在... 阅读

面向 Java Web 应用程序的 OpenID,第 1 部分:在 Java Web 应用程序中使用 OpenID 身份验证

 2010-03-25 00:00:00 来源:WEB开发网   
核心提示:OpenID 是一套分散式身份验证系统,通过 OpenID 我可以证明自己拥有类似 http://openid.jstevenperry.com/steve 这样的 URL,面向 Java Web 应用程序的 OpenID,第 1 部分:在 Java Web 应用程序中使用 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,比如 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. 获取用户提供的标识符
面向 Java Web 应用程序的 OpenID,第 1 部分:在 Java Web 应用程序中使用 OpenID 身份验证

查看原图(大图)

在本例中,用户提供的标识符是 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
面向 Java Web 应用程序的 OpenID,第 1 部分:在 Java Web 应用程序中使用 OpenID 身份验证

查看原图(大图)

此时,您需要确保有代码能处理运行于 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 的个人资料信息的示例应用程序
面向 Java Web 应用程序的 OpenID,第 1 部分:在 Java Web 应用程序中使用 OpenID 身份验证

查看原图(大图)

结束语

OpenID 用于解决大量的在线身份验证问题,已经作为一种可靠的身份管理解决方案而被广为接受。OpenID 的获取很简单,目前注册的 OpenID 已经达到数百万个。与任何其他规范一样,OpenID 身份验证 很复杂,不过 openid4java 极大地简化了它。在本文中,您已经看到了 OpenID 身份验证的运作方式。您也了解了使用 openid4java 将 OpenID 加入 Java Web 应用程序中有多么简单。

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

下载

描述名字大小下载方法
OpenID 示例openid4java-sample-app.zip4.3 MBHTTP

Tags:面向 Java Web

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接