级别: 初级 Todd Kaplinger, Project Zero 架构师和开发, IBM Gang Chen (gangchen@us.ibm.com), IBM Software Services for WebSphere, IBM
2008 年 5 月 22 日 应用程序资源的基于访问控制的安全性是 Project Zero 的核心特性之一。OpenID 是一种新兴的开放源码安全技术,它能够跨因特网进行分散的身份验证。它现在越来越受到 Web 社区的关注。Project Zero 在它的安全技术中采用了这种新技术。本文是本系列的第三篇,也是最后一篇。本文讨论 Project Zero Security 以及如何使用 OpenID 身份验证、为应用程序定义安全性规则和扩展用户注册表。
简介
在介绍 OpenID 身份验证之前,我们先简要回顾一下 Project Zero 是什么。下面引用 Project Zero 站点上的话:
Project Zero 是在 IBM 公司中启动的一个孵化项目,这个项目致力于为下一代动态 Web 应用程序提供敏捷的开发方法。Project Zero 引入了一种简单的环境,可以基于流行的 Web 技术创建、组装和执行应用程序。Project Zero 环境包括一个面向 Groovy 和 PHP 的脚本运行时,且具有应用程序编程接口,这些接口针对 REST 式服务、集成 mashup 和富 Web 界面的生成进行了优化。
 |
本文的内容
本文是包含三个部分的系列文章的最后一部分。本文讨论 Project Zero Security 以及如何使用 OpenID 身份验证、为应用程序定义安全性规则和扩展用户注册表。本文假设您已经下载了 Project Zero 并已经完成了 介绍性教程 的学习,或者自己编写过简单的应用程序。 |
|
Project Zero 主要针对的是下一代动态 Web 应用程序(通常通称为 Web 2.0),所以它主要关注交互式 Web 应用程序,这些应用程序可能会包含用户提供的内容,比如 mashup、wiki 和 blog。在 Web 2.0 时代,保持 Web 站点的安全性越来越重要了,安全性的重点在于用户提供的内容。对站点的用户进行身份验证是一项非常重要的安全需求。过去,Web 站点常常在内部维护自己的身份验证系统,包括提供一个用户注册表来存储最终用户的数字身份。
现在讨论 OpenID。OpenID 技术提供一个分散的身份验证模型,支持因特网单点登录(single sign-on,SSO)。它为最终用户提供一个可以跨 Web 使用的数字身份。通过使用 OpenID,最终用户可以拥有一个身份,从而避免将希望保护的信息暴露在外部,比如电子邮件地址。图 1 显示 OpenID 身份验证体系结构中的关键实体。
 |
什么是 OpenID?
OpenID Foundation 把 OpenID 描述为一种用于以用户为中心的数字身份的分散的开放式免费框架。OpenID 利用现有的因特网技术(URI、HTTP、SSL、Diffie-Hellman),并认识到人们已经在 blog、图像流、个人信息页面中为自己创建了身份。通过使用 OpenID,很容易把现有的 URI 转换为一个帐户,在支持 OpenID 登录的所有站点上都可以使用这个帐户。 |
|
图 1. OpenID 体系结构
OpenID 提供者(有时候也称为身份提供者 )提供注册用户身份的服务和 OpenID 身份验证。回复方(replying party)是需要对用户进行身份验证的 Web 站点。现在在访问站点时,用户不需要使用用户名和密码,而是使用单一的 OpenID 标识符,这个标识符通常是一个 URL(Uniform Resource Locator)或 XRI(eXtensible Resource Identifier)。
为了支持这种分散的身份验证模型,Project Zero 提供一个 OpenID 消费者库,让应用程序能够对第三方 OpenID 身份提供者进行身份验证。这样,应用程序开发人员就能够把管理用户信息的任务交给可信的第三方资源,这减轻了他们的工作负担,让他们可以集中精力开发应用程序。
关于示例
为了了解如何在 Project Zero 中使用 OpenID 技术,并帮助您获得实践经验,可以下载并运行示例(参见 参考资料)。这些示例涉及一系列复杂性依次增加的用例,包括以下常见的 OpenID 使用场景:
- 示例 1 是一个基本应用程序,它把身份验证类型配置为 OpenID。
- 示例 2 是在示例 1 上构建的,并提供了一个定制的用户注册表来处理各种 openid_urls 格式。
- 示例 3 是在示例 2 上构建的,并允许提供受支持的 OpenID 提供者的 “白名单”。
需要一个 OpenID 帐户
首先,需要注册一个 OpenID 帐户。参考资料 中提供了 OpenID.net 的链接,这个站点列出了各个常用的 OpenID 提供者。在本文中,我们使用最常用的 OpenID 提供者之一(myopenid.com),并用一个测试 OpenID 提供者对 OpenID 进行概念证明(注意,本文中的应用程序应该可以使用支持 OpenID 规范的任何 OpenID 提供者)。
示例应用程序 1:启用 OpenID 身份验证
为了更好地了解 Project Zero OpenID 技术,可以从 Project Zero Catalog 下载本文使用的示例应用程序 openid.demo(参见 参考资料),并在学习以下内容时参考应用程序。需要正确地配置 Eclipse IDE 和 Project Zero 插件(参考资料 中列出了这个插件的链接。可以参考本文进行设置)。准备好开发环境之后,把示例应用程序(openid.demo-1.0.0.zip)作为 Existing Project 导入工作空间。选择 archive file 作为源(见图 2)。Project Zero 通过 zero.security.openid 库提供对 OpenID 的支持,这个库已经在 ivy.xml 文件中配置为应用程序的依赖项之一。在第一次使用 OpenID 时,系统可能要求您把 zero.security.openid 库下载到存储库中。
图 2. 导入示例应用程序
单击 OK 解析依赖项。
这个示例应用程序只有两个 Web 页面,这是一个极其简单的 Project Zero 示例。第一个页面是索引页面(见图 3),用户可以在这个页面上的文本区域中发表评论。在发表评论时,这个应用程序使用 Ajax 技术向服务器发送一个 REST 式的请求来提交消息。然后,应用程序执行另一个 Ajax 调用,获取来自服务器的消息。尽管这不是一个非常有用的应用程序,但是它足以演示 OpenID 在 Project Zero 中的工作方式。运行示例应用程序的方法是右键单击 OpenIDExampleApplication 并选择 Run as – Project Zero Application。服务器组件已经启动了。现在,在浏览器中访问 http://localhost:8080 来测试示例应用程序(见图 3)。
图 3. OpenID 示例应用程序的索引页面(index.gt)
这个示例应用程序使用一个 groovy 组件(comment.groovy;见清单 1)来处理评论的发布和获取。
清单 1. comment.groovy,用来发布和获取评论的 REST 式服务
def onCreate(){
def commentJSON = zero.json.Json.decode(request.input[])
def comment = commentJSON["comment"]
if(comment != null || comment == ""){ // user submitted a comment with content
def commentId = "Comment_" + System.currentTimeMillis()
request.headers.out.Location =
zero.core.utils.URIUtils.getRequestedUri(false) + "/" + commentId
user.commentId = comment;
request.status = java.net.HttpURLConnection.HTTP_NO_CONTENT
}
}
def onRetrieve(){
def commentId = request.params.commentId.get()
def comment = user.commentId.toString();
def result = [commentId : comment]
request.view='JSON'
request.json.output = result
zero.core.views.ViewEngine.render()
}
|
配置示例应用程序的安全性
示例应用程序已经准备好了,现在需要保护这些资源,防止未经身份验证的用户访问这个站点。因为我们不希望创建和维护另一个 Web 页面和用户注册表来让用户创建和存储帐户(用户名和密码),所以决定把身份验证任务委托给第三方,同时站点可以保持资源。我们利用 Project Zero 的 OpenID 支持实现这个目标并正确地配置安全规则。
为了启用 OpenID 身份验证,需要在应用程序的 zero.config 文件指定两个配置部分(见表 1)。第一个配置部分是应用程序的全局配置。表 1 说明两个配置键(openidLoginPage 和 allowedOpenidDomains),它们都是可选的。
表 1. OpenID 应用程序配置
| 键 | 描述 | 默认值 | 必需? |
|---|
| openidLoginPage | OpenID 登录表单的 URI | openidLogin.html | 可选 | | allowedOpenidDomains | 可信 OpenID 提供者的域名列表 | 空 | 可选 |
为了在示例应用程序中启用 OpenID 身份验证,第一步是设置应用程序的 zero.config 全局配置。在这个部分中,要定义 OpenID 登录页面,让用户输入用于身份验证的 openid_url(标识符)。
清单 2. 配置 OpenID 示例 —— 应用程序配置
# specify the OpenID loginPage
@include "openid/rule.config"{
"openidLoginPage": "/openidlogin.gt"
}
|
第二步是配置资源以应用安全约束。这要定义下面三个配置设置:
-
第一部分:定义一个角色
在第一部分中定义一个角色;这是本文中第一次提到角色,角色是 Project Zero Security 的一个高级主题。这里定义的角色名为 OPENID_ROLE,属于这个组的所有用户都被称为 VALID_OPENID_USER。
-
第二部分:为 OpenID 定义一个安全约束
在第二部分中,为 index.gt 定义一个安全约束(index.gt 是这个应用程序的入口点)。这个约束规定,用户必须属于第一部分中定义的角色,才能使用这个资源。这个资源的 authType 定义为 RP(RP 代表 Relying Party,当前支持的惟一 RP 类型是 OpenID)。如果资源的 authType 为 RP,而且用户还没有登录,那么用户就被重定向到适当的登录页面进行身份验证。
-
第三部分:为 REST 式服务定义一个安全约束
在第三部分中,为 comment.groovy 定义一个安全约束,comment.groovy 是一个处理 AJAX 请求的 REST 式资源。这个约束规定,用户必须属于第一部分中定义的角色,才能使用这个资源。这个资源的 authType 是 SSO(单点登录)。使用 SSO 保护这个资源是为了方便。如果使用 RP 类型保护这个资源,那么当安全令牌过期时,AJAX 请求就被重定向到登录页面。对于非 AJAX 请求,这种情况是可以接受的;但是对于 AJAX 请求,这无法提供良好的用户体验。如果用户在尝试访问这个资源时还没有经过身份验证,那么用户会收到 401 状态码,应用程序可以提供一个特殊的错误处理函数来处理这种情况。
清单 3. 配置 OpenID 示例 —— 资源安全性
## define a role named OPENID_ROLE that only the group VALID_OPENID_USER is a member
/config/security/roles/OPENID_ROLE/groups=["VALID_OPENID_USER"]
#specify the security constraints
#protect index page
@include "security/rule.config"{
"conditions" : "/request/path =~ (/|/index.gt(/.*)?)",
"authType" : "RP",
"csrfProtect" : false,
"roles" : ["OPENID_ROLE"]
}
#protect RESTful resource with SSO so no redirects
#to login page for the case of expired tokens.
@include "security/rule.config"{
"conditions" : "/request/path =~ /resources/comment(/.*)?",
"authType" : "SSO",
"csrfProtect" : true,
"roles" : ["OPENID_ROLE"]
}
|
访问受保护的资源
在创建用户 ID(参见 “需要一个 OpenID 帐户” 一节)之后,就可以访问应用程序了。第一步是打开浏览器,访问 http://localhost:8080/index.gt,这是应用程序的主入口点。因为这个资源的 authType 是 RP,Zero Security 运行时会注意到这个资源是受保护的,并把用户重定向到上面定义的 openidLoginPage。现在应该会在浏览器中看到图 4 所示的页面。
图 4. OpenID 示例应用程序的登录表单(openidlogin.gt)
登录表单包含清单 4 所示的代码,文件名是 openidlogin.gt。
清单 4. openidlogin.gt 登录表单的 HTML 代码
<form name="input" action="" method="POST">
<table>
<tr>
<td class="header">OpenID URL: </td>
<td><input type="text" name="openid_url" value=""></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" name="submit" value="Login"></td>
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
</tr>
</table>
</form>
|
现在,我们使用 myopenid.com 提供的 openid_url(todkap.myopenid.com)登录。由于使用了 OpenID,登录请求被重定向到 myopenid.com 并在这个站点上用 opened_url 和密码进行身份验证(见图 5)。
图 5. myopenid.com 的登录表单
成功地向 OpenID 提供者(myopenid.com)证明身份之后,OpenID 提供者将把用户重定向回原来请求的页面(http://localhost:8080/index.gt),这个页面现在显示 RemoteUser、用户所属的组和角色。RemoteUser 是用来登录的 openid_url。组列表只包含一个组 VALID_OPENID_USER,角色只有 OPENID_ROLE。如果情况不是这样,用户就没有通过身份验证,因为只允许 OPENID_ROLE 角色中的用户访问这个资源。图 6 显示成功的身份验证之后浏览器应该显示的内容。
图 6. 用户登录之后的索引页面
现在,可以安全地发布评论了。
示例应用程序 2:用 Project Zero 扩展 OpenID 支持
既然基本的 OpenID 支持已经起作用了,我们就来扩展默认的事件处理函数和默认的用户注册表实现,让 OpenID 帐户能够转换为受支持的 Project Zero 本地帐户。在把因特网 SSO 与公司内部网安全系统连接起来时,这个功能非常有用。
定义定制的 resolveFederatedId 事件处理函数
resolveFederatedId 是一个事件处理函数,这个事件在请求处理期间的安全和授权事件之间引发。当用户用联邦 ID(比如 OpenID)登录时,会增加这个事件。有时候,为了让第三方身份验证者和 Project Zero 的用户注册表都能够使用用户 ID,需要对用户 ID 做一些修改(或转换)。如果 openid_url 由内部 OpenID 提供者(例如,供 IBM 内部 OpenID 身份验证使用的 'blueid.bluehost.ibm.com')拥有,那么可以只使用电子邮件地址。IBM 在各种 SSO 解决方案中常常使用电子邮件地址,包括处理 IBM 目录。清单 5 给出一个扩展 OpenIDResolver 的默认实现的事件处理函数。它覆盖了 attachUserToken 方法调用;默认实现调用这个方法来创建一个令牌,这个令牌用于在登录之后识别用户。覆盖的 attachUserToken 以经过身份验证的用户名为参数,检查它是否以变量 TRUSTED_OPENID_SERVER 开头。如果匹配,就从用户名中删除这一部分。
清单 5. 定制的 resolveFederatedId 事件处理函数代码
package zero.security.example.openid;
import zero.core.security.relying.party.openid.OpenidResolver;
public class ExampleOpenidResolver extends OpenidResolver{
private static final String TRUSTED_OPENID_SERVER =
"http://blueid.bluehost.ibm.com/?user=";
@Override
public void onResolveFederatedId() {
super.onResolveFederatedId();
}
@Override
protected void attachUserToken(String username) {
if (username.startsWith(TRUSTED_OPENID_SERVER)){
// consider this user trusted for this app and
//just use the userid in token and remoteUser
username = username.substring(TRUSTED_OPENID_SERVER.length());
}
super.attachUserToken(username);
}
}
|
清单 6 所示的配置演示如何配置定制的 resolveFederatedId 处理函数。在下载的示例中,这些代码被注释掉了。如果要在您的环境中进行测试,请务必删除这些配置项的注释标志。
清单 6. 定制的 resolveFederatedId 配置
/config/handlers += [{
"events" : "resolveFederatedId",
"handler" : "zero.security.example.openid.ExampleOpenidResolver.class"
}]
|
定义定制的 UserService 实现
除了添加定制的 resolveFederatedId 之外,我们还想演示如何创建定制的 UserService。在清单 7 所示的示例中,扩展了 OpenID 身份验证所用的默认 UserService 实现。在定制的实现类中,调用默认实现的 getUser 和 login 来获得适当的用户集。然后,添加一个定制组 DWExampleGroup,演示如何为特定用户添加额外的组。定制的 resolveFederatedId 只对内部 IBM OpenID 提供者进行检查,而定制的 UserService 对于任何 OpenID 提供者都会添加这个定制组。
清单 7. 定制的 UserRegistry 实现代码
package zero.security.example.openid;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Set;
import zero.core.security.GroupImpl;
import zero.core.security.relying.party.openid.OpenIDUserService;
import zero.core.security.userservice.UserServiceException;
public class ExampleUserService extends OpenIDUserService{
private static final Group DW_GROUP = new GroupImpl("DWExampleGroup");
@Override
public Set<Principal> getUser(String username)
throws UserServiceException {
Set<Principal> principals = super.getUser(username);
if(principals.isEmpty() == false) { // user was logged in via openID
principals.add(DW_GROUP); // add developer work group
}
return principals;
}
@Override
public Set<Principal> login(String username, String passwd)
throws UserServiceException {
Set>Principal< principals = super.login(username, passwd);
if(principals.isEmpty() == false) { // user was logged in via openID
principals.add(DW_GROUP); // add developer work group
}
return principals;
}
}
|
清单 8 中的配置演示如何配置定制的 UserService.
清单 8. 定制 UserService 的配置
registryImplStr="zero.security.example.openid.ExampleUserService"
/config/security/userservice/registryType="exampleUserService"
# must implement zero.core.security.userservice.UserService
/config/security/userservice/registryImpl/exampleUserService=${registryImplStr}
|
演示示例 2
我们已经给应用程序配置了定制的 resolveFederatedId 和 UserService 实现,现在需要重新启动应用程序,让配置生效。现在再次访问受保护的应用程序。在前一个示例中,我们使用的 openid_url 是 todkap.myopenid.com。这一次使用的 openid_url 是 http://blueid.bluehost.ibm.com/?user=todkap@us.ibm.com,这是为了演示新配置的特性。
首先进入与前面一样的登录表单。但是,这一次用 IBM OpenID 服务提供的 openid_url(http://blueid.bluehost.ibm.com/?user=todkap@us.ibm.com)登录。这会把用户重定向到 OpenID 提供者登录页面(见图 7):
图 7. IBM OpenID 提供者的登录表单
身份验证请求现在被转发到 IBM 内部网 OpenID 站点 blueid.bluehost.ibm.com,而不是 myopenid.com。向 OpenID 提供者成功地验证身份之后,OpenID 提供者将把用户重定向回原来请求的页面(http://localhost:8080/index.gt),这个页面现在显示 RemoteUser、用户所属的组和角色。这里的差异在于,远程用户是用来登录的 openid_url 经过修改后的版本(注意:resolveFederatedId 修改了这个用户 ID,只留下用户的电子邮件地址)。组列表现在包含两个组:原来的 'VALID_OPENID_USER' 和通过 UserService 新添加的 'DWExampleGroup'。角色是 OPENID_ROLE。如果情况不是这样,用户就没有通过身份验证,因为只允许 'OPENID_ROLE' 角色中的用户访问这个资源。图 8 显示成功的身份验证之后浏览器应该显示的内容。
图 8. 用户登录之后的索引页面
现在,可以发布评论了。
示例应用程序 3:只使用可信的 OpenID 提供者
我们已经看到了如何在两个常见场景中使用 OpenID。OpenID 的缺点之一是,提供 OpenID 提供者服务的任何站点都可以声明用户的身份是有效的。回复方(您的 Web 站点)很难信任所有这些声明。为了解决这个问题,Zero 提供了一个配置选项,用来限制应用程序可信任的 OpenID 提供者站点。清单 9 中的示例演示如何启用这个配置,以及如果指定的 openid_url 来自不可信域,会发生什么情况。注意,尽管这个功能可以通过 JavaScript 代码在客户端上实现,但是检验总是应该在服务器上进行,以防止攻击者篡改客户机代码。
清单 9. 配置可信 OpenID 提供者的白名单
@include "openid/rule.config"{
"openidLoginPage" : "/openidlogin.gt",
"allowedOpenidDomains" = ["myopenid.com",
"verisignlabs.com", "blueid.bluehost.ibm.com"]
}
|
我们已经配置了这个选项,现在重新启动应用程序并尝试使用一个不可信的 OpenID 提供者。在这个示例中,使用一个虚构的 openid_url 'http://myuntrusteduser.someunknownsite.com'。这会导致客户机上产生一个错误消息,它指出 openid_url 的提供者没有列在 allowedOpenidDomains 列表中(见图 9)。
图 9. 不可信 OpenID 提供者错误页面
结束语
OpenID 使应用程序能够依靠第三方身份验证提供者来处理身份验证,从而提高了应用程序部署的灵活性。因为越来越多的用户希望只用一个用户身份访问多个站点(包括 blog、wiki 和其他社会网络活动),所以 OpenID 这样的提供者会越来越普及。另外,许多 Web 站点不愿意自行维护用户个人信息,也不愿意要求用户提供这些信息。我们希望本文能够帮助您掌握如何在 Project Zero 平台上使用 OpenID 技术实现这种分散的身份验证,希望整个系列能够帮助您在 Zero 应用程序中实现所有重要的安全特性。作为由用户驱动、快速发展的 Web 2.0 应用程序的开发人员,您必须意识到安全性对于您的用户和业务是多么重要。
参考资料 学习
获得产品和技术
讨论
作者简介  | 
|  | Todd Kaplinger 是 IBM Software Group 的一名高级软件工程师,现任 Project Zero Security 团队的负责人和架构师。Todd 是诸如 JSP、Servlet 和 PHP 这类基于 Web 的技术专家,目前专注于研究新兴的 Web 2.0 技术及该技术对企业的影响。Todd 现在是负责 JSR 223: Scripting for the
Java Platform 的专家组成员,,他也是 Open AJAX Alliance 的一员,致力于各种安全性方面的 Ajax 框架实现间的互操作性研究。Todd
曾担任过 IBM WebSphere Webcontainer 和 Remote Request Dispatcher (RRD) 项目的团队负责人和架构师,并曾作为 IBM Servlet 专家组的代表参与 JSR 154 Servlet 2.5 规范研究。 |
对本文的评价
|