使用 Acegi 保护 Java 应用程序,第 2 部分: 使用 LDAP 目录服务器

使用 ApacheDS 和 Acegi 实现访问控制

了解了 Acegi 安全系统(Acegi Security System)的 基础知识 后,我们将介绍该系统的更加高级的应用。在本文中,Bilal Siddiqui 向您展示了如何结合使用 Acegi 和一个 LDAP 目录服务器,实现灵活的具有高性能的 Java™ 应用程序的安全性。还将了解如何编写访问控制策略并将其存储在 ApacheDS 中,然后配置 Acegi 使其与目录服务器交互,从而实现身份验证和授权的目的。

Bilal Siddiqui, 自由顾问, WaxSys

Bilal Siddiqui 是一名电子工程师、XML 顾问,他还是 WaxSys(主要从事电子商务简化)的创建者之一。自从 1995 年毕业于拉合尔工程技术大学(University of Engineering and Technology,Lahore)电子工程专业以后,他就开始为工业控制系统设计各种软件解决方案。稍后,他致力于 XML 方面并使用他在 C++ 编程中取得的经验来构建基于 Web 和 WAP 的 XML 处理工具、服务器端解析方案和服务应用程序。Bilal 是一名技术推广者,并且是一名多产的技术作家。



2007 年 6 月 21 日

这期共分三部分的系列文章介绍了如何使用 Acegi 安全系统保护 Java 企业应用程序。在 本系列第一篇文章 中,我介绍了 Acegi 并解释了如何使用安全过滤器实现一个简单的基于 URL 的安全系统。在第二篇文章中,我将讨论 Acegi 的更加高级的应用,首先我将编写一个访问控制策略并将其存储在 ApacheDS 中,ApacheDS 是一个开源的 LDAP 目录服务器。我还将展示配置 Acegi 的方法,使它能够与目录服务器交互并实现您的访问控制策略。本文的结尾提供了一个示例应用程序,它使用 ApacheDS 和 Acegi 实现了一个安全的访问控制策略。

实现访问控制策略通常包含两个步骤:

  1. 将有关用户和用户角色的数据存储在目录服务器中。
  2. 编写安全代码,它将定义有权访问并使用数据的人员。

Acegi 将减轻代码编写的工作,因此在这篇文章中,我将展示如何将用户和用户角色信息存储到 ApacheDS 中,然后实现这些信息的访问控制策略。在该系列的最后一篇文章中,我将展示如何配置 Acegi,实现对 Java 类的安全访问。

您可以在本文的任何位置 下载样例应用程序。参见 参考资料 下载 Acegi、Tomcat 和 ApacheDS,您需要使用它们运行样例代码和示例应用程序。

LDAP 基础知识

轻量级目录访问协议(Lightweight Directory Access Protocol,LDAP)可能是最流行的一种定义数据格式的协议,它针对常见的目录操作,例如对存储在目录服务器中的信息执行的读取、编辑、搜索和删除操作。本节将简要解释为什么目录服务器是属性文件存储安全信息的首选,并展示如何在 LDAP 目录中组织和托管用户信息。

为什么要使用目录服务器?

本系列第一部分向您介绍了一种简单的方法,可以将用户信息以属性文件的形式保存起来(参见 第 1 部分,清单 6)。属性文件以文本格式保存用户名、密码和用户角色。对于大多数真实应用程序而言,使用属性文件存储安全信息远远不够。各种各样的理由表明,目录服务器通常都是更好的选择。其中一个原因是,真实的企业应用程序可以被大量用户访问 —— 通常是几千名用户,如果应用程序将其部分功能公开给用户和供应商时更是如此。频繁搜索文本文件中随意存储的信息,这样做的效率并不高,但是目录服务器对这类搜索进行了优化。

第 1 部分的清单 6 中的属性文件演示了另一个原因,该文件组合了用户和角色。在真实的访问控制应用程序中,您通常都需要分别定义和维护用户和角色信息,这样做可以简化用户库的维护。目录服务器为更改或更新用户信息提供了极大的灵活性,例如,反映职位升迁或新聘用人员。参见 参考资料 以了解更多关于目录服务器的使用及其优点的信息。

LDAP 目录设置

如果希望将用户信息存储在一个 LDAP 目录中,您需要理解一些有关目录设置的内容。本文并没有提供对 LDAP 的完整介绍(参见 参考资料),而是介绍了一些在尝试结合使用 Acegi 和 LDAP 目录之前需要了解的基本概念。

LDAP 目录以节点树的形式存储信息,如图 1 所示:

图 1. LDAP 目录的树状结构
图 1. LDAP 目录的树状结构

在图 1 中,根节点的名称为 org。根节点可以封装与不同企业有关的数据。例如,本系列第 1 部分开发的制造业企业被显示为 org 节点的直接子节点。该制造业企业具有两个名为 departmentspartners 的子节点。

partners 子节点封装了不同类型的合作伙伴。图 1 所示的三个分别为 customersemployeessuppliers。注意,这三种类型的合作伙伴其行为与企业系统用户一样。每一种类型的用户所扮演的业务角色不同,因此访问系统的权利也不同。

类似地,departments 节点包含该制造业企业的不同部门的数据 —— 例如 engineeringmarketing 字节点。每个部门节点还包含一组或多组用户。在 图 1 中,engineers 组是 engineering 部门的子节点。

假设每个部门的子节点表示一组用户。因此,部门节点的子节点具有不同的用户成员。例如,设计部门的所有工程师都是 engineering 部门内 engineers 组的成员。

最后,注意 图 1departments 节点的最后一个子节点。specialUser 是一名用户,而非一组用户。在目录设置中,像 alicebob 之类的用户一般都包含在 partners 节点中。我将这个特殊用户包含在 departments 节点中,以此证明 Acegi 允许用户位于 LADP 目录中任何地点的灵活性。稍后在本文中,您将了解如何配置 Acegi 以应用 specialUser

使用专有名称

LDAP 使用专有名称(DN)的概念来识别 LDAP 树上特定的节点。每个节点具有惟一的 DN,它包含该节点完整的层次结构信息。例如,图 2 展示了图 1 中的一些节点的 DN:

图 2. LDAP 目录节点的专有名称
图 2. LDAP 目录节点的专有名称

首先,注意图 2 中根节点的 DN。它的 DN 为 dc=org,这是与 org 根节点相关的属性值对。每个节点都有若干个与之相关的属性。dc 属性代表 “domain component” 并由 LDAP RFC 2256 定义(参见 参考资料 中有关官方 RFC 文档的链接),LDAP 目录中的根节点通常表示为一个域组件。

每个 LDAP 属性是由 RFC 定义的。LDAP 允许使用多个属性创建一个 DN,但是本文的示例只使用了以下 4 个属性:

  • dc(域组件)
  • o(组织)
  • ou(组织单元)
  • uid(用户 ID)

示例使用 dc 表示域,用 o 表示组织名称,ou 表示组织的不同单元,而 uid 表示用户。

由于 org 是根节点,其 DN 只需指定自身的名称(dc=org)。比较一下,manufacturingEnterprise 节点的 DN 是 o=manufacturingEnterprise,dc=org。当向下移动节点树时,每个父节点的 DN 被包含在其子节点的 DN 中。

将属性分组

LDAP 将相关的属性类型分到对象类中。例如,名为 organizationalPerson 的对象类所包含的属性定义了在组织内工作的人员(例如,职称、常用名、邮寄地址等等)。

对象类使用继承特性,这意味着 LDAP 定义了基类来保存常用属性。然后子类再对基类进行扩展,使用其定义的属性。LDAP 目录中的单个节点可以使用若干个对象类:本文的示例使用了以下几个对象类:

  • top 对象类是 LDAP 中所有对象类的基类。
  • 当其他对象类都不适合某个对象时,将使用 domain 对象类。它定义了一组属性,任何一个属性都可以用来指定一个对象。其 dc 属性是强制性的。
  • organization 对象类表示组织节点,例如 图 2 中的 manufacturingEnterprise
  • organizationalUnit 对象类表示组织内的单元,例如 图 1 中的 departments 节点及其子节点。
  • groupOfNames 对象类表示一组名称,例如某部门职员的名称。它具有一个 member 属性,该属性包含一个用户列表。图 1 中所有的组节点(例如 engineers 节点)使用 member 属性指定该组的成员。而且,示例使用 groupOfNames 对象类的 ou(组织单元)属性指定组用户的业务角色。
  • organizationalPerson 对象类表示组织内某个职员(例如 图 1 中的 alice 节点)。

使用 LDAP 服务器

在真实的应用程序中,通常将有关系统用户的大量信息托管在一个 LDAP 目录中。例如,将存储每个用户的用户名、密码、职称、联系方式和工资信息。为简单起见,下面的例子将只向您展示如何保存用户名和密码。

如前所述,示例使用 ApacheDS(一种开源的 LDAP 目录服务器)演示了 Acegi 是如何使用 LDAP 目录的。示例还使用了一个开源的 LDAP 客户机(名为 JXplorer)执行简单的目录操作,例如将信息托管在 ApacheDS 上。参见 参考资料 以下载 ApacheDS、JXplorer 并了解更多有关两者协作的信息。

在 ApacheDS 创建一个根节点

要创建 图 1 所示的节点树,必须首先在 ApacheDS 中创建一个根节点 org。ApacheDS 为此提供了一个 XML 配置文件。XML 配置文件定义了一组可进行配置的 bean,从而根据应用程序的需求定制目录服务器的行为。本文只解释创建根节点所需的配置。

可以在 ApacheDS 安装中的 conf 文件夹找到名为 server.xml 的 XML 配置文件。打开文件后,会发现很多 bean 配置类似于 Acegi 的过滤器配置。查找名为 examplePartitionsConfiguration 的 bean。该 bean 控制 ApacheDS 上的分区。当创建新的根节点时,实际上将在 LDAP 目录上创建一个新的分区。

编辑 examplePartitionConfiguration bean 以创建 org 根节点,如清单 1 所示:

清单 1. 编辑模式的 examplePartitionConfiguration bean 配置
<bean id="examplePartitionConfiguration" class=
"org.apache.directory.server.core.partition.impl.btree.MutableBTreePartitionConfiguration"
>

<property name="suffix"><value>dc=org</value></property>

<property name="contextEntry">
<value>
objectClass: top
objectClass: domain
dc: org
</value>
</property>

<!-- Other properties of the examplePartitionConfiguration bean, which you don't  
need to edit. -->

</bean>

清单 1 编辑了 examplePartitionConfiguration bean 的两个属性:

  • 一个属性名为 suffix,它定义根条目的 DN。
  • 另一个属性名为 contextEntry,定义 org 节点将使用的对象类。注意,org 根节点使用两个对象类:topdomain

本文的 源代码下载 部分包含了编辑模式的 server.xml 文件。如果希望继续学习本示例,请将 server.xml 文件从源代码中复制到您的 ApacheDS 安装目录中的正确位置,即 conf 文件夹。

图 3 所示的屏幕截图展示了在 ApacheDS 中创建根节点后,JXplorer 是如何显示该根节点的:

图 3. JXplorer 显示根节点
图 3. JXplorer 显示根节点

填充服务器

设置 LDAP 服务器的下一步是使用用户和组信息填充服务器。您可以使用 JXplorer 在 ApacheDS 中逐个创建节点,但是使用 LDAP Data Interchange Format (LDIF) 填充服务器会更加方便。LDIF 是可被大多数 LDAP 实现识别的常见格式。developerWorks 文章很好地介绍了 LDIF 文件的内容,因此本文将不再做详细说明。(参见 参考资料 中有关 LDIF 的详细资料。)

您可以在 源代码下载 部分查看 LDIF 文件,它表示 图 1 所示的用户和部门。您可以使用 JXplorer 将 LDIF 文件导入到 ApacheDS。要导入 LDIF 文件,在 JXplorer 中使用 LDIF 菜单,如图 4 所示:

图 4. 将 LDIF 文件导入到 ApacheDS
图 4. 将 LDIF 文件导入到 ApacheDS

将 LDIF 文件导入到 ApacheDS 之后,JXplorer 将显示用户节点和部门节点树,如 图 1 所示。现在您可以开始配置 Acegi,使其能够与您的 LDAP 服务器通信。


为 LDAP 实现配置 Acegi

回想一下第 1 部分,其中 Acegi 使用身份验证处理过滤器(Authentication Processing Filter,APF)进行身份验证。APF 执行所有后端身份验证处理任务,例如从客户机请求中提取用户名和密码,从后端用户库读取用户参数,以及使用这些信息对用户进行身份验证。

您在第 1 部分中为属性文件实现配置了 APF,现在您已将用户库存储在 LDAP 目录中,因此必须使用不同的方式配置过滤器来和 LDAP 目录进行通信。首先看一下清单 2,它展示了在第 1 部分中的 “Authentication Processing Filter” 一节中如何为属性文件实现配置 APF 过滤器:

清单 2. 为属性文件配置 APF
<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">

<property name="authenticationManager" ref="authenticationManager" />

<property name="authenticationFailureUrl"
value="/login.jsp?login_error=1" />

<property name="defaultTargetUrl"
value="/index.jsp" />

<property name="filterProcessesUrl"
value="/j_acegi_security_check" />

</bean>

查看一下清单 2,您曾经为 APF 提供了 4 个参数。您只需在 LDAP 服务器中为存储重新配置第一个参数(authenticationManager)即可。其他三个参数保持不变。

配置身份验证管理器

清单 3 展示了如何配置 Acegi 的身份验证管理器,以实现与 LDAP 服务器的通信:

清单 3. 为 LDAP 配置 Acegi 的身份验证管理器
<bean id="authenticationManager"
class="org.acegisecurity.providers.ProviderManager">

<property name="providers">
<list>
<ref local="ldapAuthenticationProvider" />
</list>
</property>

</bean>

在清单 3 中,org.acegisecurity.providers.ProviderManager 是一个管理器类,它管理 Acegi 的身份验证过程。为此,身份验证管理器需要一个或多个身份验证提供者。您可以使用管理器 bean 的提供者属性来配置一个或多个提供者。清单 3 只包含了一个提供者,即 LDAP 身份验证提供者。

LDAP 身份验证提供者处理所有与后端 LDAP 目录的通信。您必须对其进行配置,下一节内容将讨论该主题。

配置 LDAP 身份验证提供者

清单 4 展示了 LDAP 身份验证提供者的配置:

清单 4. 配置 LDAP 身份验证提供者
<bean id="ldapAuthenticationProvider"
class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider">

<constructor-arg><ref local="authenticator"/></constructor-arg>

<constructor-arg><ref local="populator"/></constructor-arg>

</bean>

注意 LDAP 身份验证提供者类的名称为 org.acegisecurity.providers.ldap.LdapAuthenticationProvider 。其构造函数包含两个参数,使用两个 <constructor-arg> 标记的形式,如清单 4 所示。

LdapAuthenticationProvider 构造函数的第一个参数是 authenticator,该参数通过检查用户的用户名和密码对 LDAP 目录的用户进行身份验证。完成身份验证后,第二个参数 populator 将从 LDAP 目录中检索有关该用户的访问权限(或业务角色)信息。

以下小节将向您展示如何配置验证器和填充器 bean。


配置验证器

authenticator bean 将检查具有给定用户名和密码的用户是否存在于 LDAP 目录中。Acegi 提供了名为 org.acegisecurity.providers.ldap.authenticator.BindAuthenticator 的验证器类,它将执行验证用户名和密码所需的功能。

配置 authenticator bean,如清单 5 所示:

清单 5. 配置验证器 bean
<bean id="authenticator"
class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">

<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>

<property name="userDnPatterns">
<list>
<value>uid={0},ou=employees,ou=partners</value>
<value>uid={0},ou=customers,ou=partners</value>
<value>uid={0},ou=suppliers,ou=partners</value>
</list>
</property>

<property name="userSearch"><ref local="userSearch"/></property>

</bean>

在清单 5 中,BindAuthenticator 构造函数具有一个参数,使用 <constructor-arg> 标记的形式。清单 5 中参数的名称为 initialDirContextFactory。该参数实际上是另一个 bean,稍后您将学习如何配置该 bean。

目前为止,只知道 initialDirContextFactory bean 的作用就是为稍后的搜索操作指定初始上下文。初始上下文是一个 DN,它指定了 LDAP 目录内某个节点。指定初始上下文后,将在该节点的子节点中执行所有的搜索操作(例如查找特定用户)。

例如,回到 图 2 中查看 partners 节点,它的 DN 是 ou=partners,o=manufacturingEnterprise,dc=org。如果将 partners 节点指定为初始上下文,Acegi 将只在 partners 节点的子节点中查找用户。

指定 DN 模式

除配置 BindAuthenticator 构造函数外,还必须配置 authenticator bean 的两个属性(清单 5 中的两个 <property> 标记)。

第一个 <property> 标记定义了一个 userDnPatterns 属性,它封装了一个或多个 DN 模式列表。DN 模式 指定了一组具有类似特性的 LDAP 节点(例如 图 2 所示的 employees 节点的所有子节点)。

Acegi 的身份验证器从 authenticator bean 的 userDnPatterns 属性中配置的每个 DN 模式构造了一个 DN。例如,查看 清单 5 中配置的第一个模式,即 uid={0},ou=employees,ou=partners。在进行身份验证的时候,authenticator bean 使用用户提供的用户名(比如 alice)替换了 {0}。使用用户名取代了 {0} 之后,DN 模式将变为相对 DN(RDN)uid=alice,ou=employees,ou=partners,它需要一个初始上下文才能成为 DN。

例如,查看 图 2 中的 alice's 条目。该条目是 employees 节点的第一个子节点。它的 DN 是 uid=alice,ou=employees,ou=partners,o=manufacturingEnterprise, dc=org。如果使用 o=manufacturingEnterprise,dc=org 作为初始上下文并将其添加到 RDN uid=alice,ou=employees,ou=partners 之后,将获得 alice 的 DN。

使用这种方法通过 DN 模式构建了用户的 DN 后,authenticator 将把 DN 和用户密码发送到 LDAP 目录。目录将检查该 DN 是否具有正确的密码。如果有的话,用户就可以通过身份验证。这个过程在 LDAP 术语中被称为 bind 身份验证。LDAP 还提供了其他类型的身份验证机制,但是本文的示例只使用了 bind 身份验证。

如果目录中并没有第一个 DN 模式创建的 DN,authenticator bean 尝试使用列表中配置的第二个 DN 模式。依此类推,authenticator bean 将尝试所有的 DN 模式来为进行身份验证的用户构造正确的用户 DN。

搜索过滤器

回想一下较早的章节 “LDAP 目录设置”,我在将用户信息存储到 LDAP 目录时添加了一点灵活性。方法是在 图 1 所示的 departments 节点内创建一个特定用户(specialUser)。

如果试图使用 清单 5 中配置的任何一种 DN 模式创建特定用户的 DN,您会发现没有一种 DN 模式可用。因此,当用户尝试登录时,Acegi 的 authenticator bean 将不能够构造正确的 DN,从而无法对该用户进行身份验证。

通过允许您指定搜索过滤器,Acegi 能够处理类似的特殊情况。身份验证器 bean 使用搜索过滤器查找不能够通过 DN 模式构造 DN 进行身份验证的用户。

清单 5 中的第二个 <property> 标记具有一个 <ref> 子标记,它引用名为 userSearch 的 bean。userSearch bean 指定搜索查询。清单 6 展示了如何配置 userSearch bean 来处理特定用户:

清单 6. 配置搜索查询以搜索特定用户
<bean id="userSearch"
class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">

<constructor-arg>
<value>ou=departments</value>
</constructor-arg>

<constructor-arg>
<value>(uid={0})</value>
</constructor-arg>

<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>

<property name="searchSubtree">
<value>true</value>
</property>

</bean>

搜索查询的参数

清单 6 展示了 userSearch bean 是 org.acegisecurity.ldap.search.FilterBasedLdapUserSearch 类的一个实例,该类的构造函数具有三个参数。第一个参数指定 authenticator 在哪个节点中搜索用户。第一个参数的值为 ou=departments,该值是一个 RDN,指定了 图 2 所示的 departments 节点。

第二个参数 (uid={0}) 指定了一个搜索过滤器。由于使用 uid 属性指定用户,因此可以通过查找 uid 属性具有特定值的节点来查找用户。正如您所料,花括号里面的 0 向 Acegi 表示使用进行身份验证的用户的用户名(本例中为 specialUser)替换 {0}

第三个参数是对讨论 清单 5 中的 BindAuthenticator 构造函数时引入的相同初始上下文的引用。回想一下,当指定了初始上下文后,稍后将在该初始上下文节点的子节点内进行所有的搜索操作。注意,应将指定为 清单 5 中第一个参数(ou=departments)的值的 RDN 前加到初始上下文。

除了这三个构造器参数,清单 6 所示的 userSearch bean 还具有一个名为 searchSubtree 的属性。如果将其值指定为 true,搜索操作将包括节点的子树(即所有子节点、孙节点、孙节点的子节点等),该节点被指定为构造函数的第一个参数的值。

authenticator bean 的配置完成后,下一步将查看 populator bean 的配置,如 清单 4 所示。


配置 populator

populator bean 将读取已经通过 authenticator bean 身份验证的用户的业务角色。清单 7 展示 populator bean 的 XML 配置:

清单 7. populator bean 的 XML 配置
<bean id="populator"
class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">

<constructor-arg>
<ref local="initialDirContextFactory"/>
</constructor-arg>

<constructor-arg>
<value>ou=departments</value>
</constructor-arg>

<property name="groupRoleAttribute">
<value>ou</value>
</property>

<property name="searchSubtree">
<value>true</value>
</property>

</bean>

在清单 7 中,populator bean 的构造函数包括 2 个参数,以及一个 groupRoleAttribute 属性。构造函数的第一个参数指定了 populator bean 用来读取经过验证用户的业务角色的初始上下文。并不强制要求 authenticatorpopulator bean 使用相同的初始上下文。您可以为这两者分别配置一个初始上下文。

第二个构造函数参数指定了 populator 前加到初始上下文的 RDN。因此,RDN 组成了包含组用户的节点的 DN,例如 departments 节点。

populator bean 的 groupRoleAttribute 属性指定了持有组成员业务角色数据的属性。回想 设置 LDAP 目录 一节中,您将每组用户的业务角色信息存储在名为 ou 的属性中。然后将 ou 设置为 groupRoleAttribute 属性的值,如 清单 7 所示。

如您所料,populator bean 将搜索整个 LDAP 目录来查找经过验证的用户所属的组节点。然后读取组节点的 ou 属性的值,获取用户经过授权的业务角色。

这样就完成了 populator bean 的配置。目前为止,我们在三个位置使用了初始上下文:清单 5清单 6清单 7。接下来将了解如何配置初始上下文。

配置初始上下文

清单 8 展示了在 Acegi 中配置初始上下文的过程:

清单 8. 初始上下文的 XML 配置
<bean id="initialDirContextFactory"
class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">

<constructor-arg value="ldap://localhost:389/o=manufacturingEnterprise,dc=org"/>

<property name="managerDn">
<value>cn=manager,o=manufacturingEnterprise,dc=org</value>
</property>

<property name="managerPassword">
<value>secret</value>
</property>

</bean>

清单 8 中 Acegi 的初始上下文类的名称为 org.acegisecurity.ldap.DefaultInitialDirContextFactory,这是 Acegi 包含的工厂类。Acegi 在内部使用该类构造其他处理目录操作(例如在整个目录中搜索)的类的对象。当配置初始上下文工厂时,必须指定以下内容:

  • 将您的 LDAP 目录和根目录节点的网络地址指定为构造函数的参数。在初始上下文配置的节点将作为根节点。就是说所有后续操作(例如 search)都将在根节点定义的子树中执行。
  • 将 DN 和密码分别定义为 managerDnmanagerPassword 属性。在执行任何搜索操作之前,Acegi 必须使用目录服务器对 DN 和密码进行身份验证。

您已经了解了如何将用户库托管在 LDAP 目录中,以及如何配置 Acegi 来使用来自 LDAP 目录的信息对用户进行身份验证。下一节将进一步介绍 Acegi 的身份验证处理过滤器,了解新配置的 bean 是如何管理身份验证过程的。


身份验证和授权

APF 配置完成后,将能够与 LDAP 目录进行通信来对用户进行身份验证。如果您阅读过第 1 部分,那么对与目录通信过程中 APF 执行的一些步骤不会感到陌生,我在第 1 部分中向您展示了过滤器如何使用不同的服务进行用户身份验证。图 5 所示的序列表与您在 第 1 部分图 3 看到的非常类似:

图 5. APF 对一名 LDAP 用户进行身份验证
图 5. APF 对一名 LDAP 用户进行身份验证

无论 APF 使用属性文件进行内部的身份验证还是与 LDAP 服务器进行通信,步骤 1 到步骤 9 与第 1 部分是相同的。这里简单描述了前 9 个步骤,您可以从步骤 10 开始继续学习特定于 LDAP 的事件:

  1. 过滤器链前面的过滤器将请求、响应和过滤器链对象传递给 APF。
  2. APF 使用取自请求对象的用户名、密码和其他信息创建一个身份验证标记。
  3. APF 将身份验证标记传递给身份验证管理器。
  4. 身份验证管理器可能包含一个或多个身份验证提供者。每个提供者恰好支持一种身份验证类型。管理器将检查哪一种提供者支持从 APF 接收到的身份验证标记。
  5. 身份验证管理器将身份验证标记传递给适合该类型身份验证的提供者。
  6. 身份验证提供者从身份验证标记中提取用户名并将其传递到名为 user cache service 的服务。Acegi 缓存了已经进行过身份验证的用户。该用户下次登录时,Acegi 可以从缓存中加载他或她的详细信息(比如用户名、密码和权限),而不是从后端数据存储中读取数据。这种方法使得性能得到了改善。
  7. user cache service 检查用户的详细信息是否存在于缓存中。
  8. user cache service 将用户的详细信息返回给身份验证提供者。如果缓存不包含用户详细信息,则返回 null。
  9. 身份验证提供者检查缓存服务返回的是用户的详细信息还是 null。
  10. 从这里开始,身份验证处理将特定于 LDAP。 如果缓存返回 null,LDAP 身份验证提供者将把用户名(在步骤 6 中提取的)和密码传递给 清单 5 中配置的 authenticator bean。
  11. authenticator 将使用在 清单 5userDnPatterns 属性中配置的 DN 模式创建用户 DN。通过从一个 DN 模式中创建一个 DN,然后将该 DN 和用户密码(从用户请求中获得)发送到 LDAP 目录,它将逐一尝试所有可用的 DN 模式。LDAP 目录将检查该 DN 是否存在以及密码是否正确。如果其中任何一个 DN 模式可行的话,用户被绑定到 LDAP 目录中,authenticator 将继续执行步骤 15。
  12. 如果任何一种 DN 模式都不能工作的话(这意味着在 DN 模式指定的任何位置都不存在使用给定密码的用户),authenticator 根据 清单 6 配置的搜索查询在 LDAP 目录中搜索用户。如果 LDAP 目录没有找到用户,那么身份验证以失败告终。
  13. 如果 LDAP 目录查找到了用户,它将用户的 DN 返回到 authenticator
  14. authenticator 将用户 DN 和密码发送到 LDAP 目录来检查用户密码是否正确。如果 LDAP 目录发现用户密码是正确的,该用户将被绑定到 LDAP 目录。
  15. authenticator 将用户信息发送回 LDAP 身份验证提供者。
  16. LDAP 身份验证提供者将控制权传递给 populator bean。
  17. populator 搜索用户所属的组。
  18. LDAP 目录将用户角色信息返回给 populator
  19. populator 将用户角色信息返回给 LDAP 身份验证提供者。
  20. LDAP 身份验证提供者将用户的详细信息(以及用户业务角色信息)返回给 APF。用户现在成功进行了身份验证。

不论使用何种身份验证方法,最后三个步骤是相同的(步骤21、21 和 23)。

配置拦截器

您已经了解了 APF 对用户进行身份验证的步骤。接下来是查看成功进行身份验证的用户是否被授权访问所请求的资源。这项任务由 Acegi 的拦截过滤器(Interceptor Filter,IF)完成。本节将向您展示如何配置 IF 来实现访问控制策略。

回想一下在 第 1 部分的清单 7 中配置 IF 的步骤。拦截过滤器在资源和角色之间建立映射,就是说只有具备必要角色的用户才能访问给定资源。为了演示制造业企业中不同部门的业务角色,清单 9 向现有的 IF 配置添加了另外的角色:

清单 9. 配置拦截过滤器
<bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">

<property name="authenticationManager" ref="authenticationManager" />

<property name="accessDecisionManager" ref="accessDecisionManager" />

<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/protected/engineering/**=ROLE_HEAD_OF_ENGINEERING
/protected/marketing/**=ROLE_HEAD_OF_MARKETING
/**=IS_AUTHENTICATED_ANONYMOUSLY
</value>
</property>

</bean>

在清单 9 中,IF 包含三个参数。其中第一个和第三个参数与第 1 部分中最初配置的参数相同。这里添加了第二个参数(名为 accessDecisionManager 的 bean)。

accessDecisionManager bean 负责指定授权决策。它使用清单 9 中第三个参数提供的访问控制定义来指定授权(或访问控制)决策。第三个参数是 objectDefinitionSource

配置访问决策管理器

accessDecisionManager 决定是否允许一个用户访问某个资源。Acegi 提供了一些访问决策管理器,它们指定访问控制决策的方式有所不同。本文只解释了其中一种访问决策管理器的工作方式,其配置如清单 10 所示:

清单 10. 配置访问决策管理器
<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">

<property name="decisionVoters">
<list>
<bean class="org.acegisecurity.vote.RoleVoter"/>
<bean class="org.acegisecurity.vote.AuthenticatedVoter" />
</list>
</property>

</bean>

在清单 10 中,accessDecisionManager bean 是 org.acegisecurity.vote.AffirmativeBased 类的实例。accessDecisionManager bean 只包含一个参数,即投票者(voter)列表。

在 Acegi 中,投票者确定是否允许某个用户访问特定的资源。当使用 accessDecisionManager 查询时,投票者具有三个选项:允许访问(access-granted)、拒绝访问(access-denied),如果不确定的话则放弃投票(abstain from voting)。

不同类型的访问决策管理器解释投票者决策的方法也有所不同。清单 10 所示的 AffirmativeBased 访问决策管理器实现了简单的决策逻辑:如果任何投票者强制执行肯定投票,将允许用户访问所请求的资源。

投票者逻辑

Acegi 提供了若干个投票者实现类型。accessDecisionManager 将经过验证的用户的信息(包括用户的业务角色信息)和 objectDefinitionSource 对象传递给投票者。本文的示例使用了两种类型的投票者,RoleVoterAuthenticatedVoter,如清单 10 所示。现在看一下每种投票者的逻辑:

  • RoleVoter 只有在 objectDefinitionSource 对象的行中找到以 ROLE_ 前缀开头的角色时才进行投票。如果 RoleVoter 没有找到这样的行,将放弃投票;如果在用户业务角色中找到一个匹配的角色,它将投票给允许访问;如果没有找到匹配的角色,则投票给拒绝访问。在 清单 9 中,有两个角色具有 ROLE_ 前缀:ROLE_HEAD_OF_ENGINEERINGROLE_HEAD_OF_MARKETING
  • AuthenticatedVoter 只有在 objectDefinitionSource 对象中找到具有某个预定义角色的行时才进行投票。在 清单 9 中,有这样一行:IS_AUTHENTICATED_ANONYMOUSLY。匿名身份验证意味着用户不能够进行身份验证。找到该行后,AuthenticatedVoter 将检查一个匿名身份验证的用户是否可以访问某些不受保护的资源(即这些资源没有包含在具备 ROLE_ 前缀的行中)。如果 AuthenticatedVoter 发现所请求的资源是不受保护的并且 objectDefinitionSource 对象允许匿名身份验证的用户访问不受保护的资源,它将投票给允许访问;否则就投票给拒绝访问。

示例应用程序

本文提供了一个示例应用程序,它将演示您目前掌握的 LDAP 和 Acegi 概念。LDAP-Acegi 应用程序将显示一个索引页面,该页面将设计和销售文档呈现给合适的经过身份验证的用户。正如您将看到的一样,LDAP-Acegi 应用程序允许用户 alice 查看设计文档,并允许用户 bob 查看销售文档。它还允许特定用户同时查看设计和销售文档。所有这些内容都是在本文开头配置 LDAP 目录服务器时设置的。立即 下载示例应用程序 来开始使用它。


结束语

在本文中,您了解了如何将用户和业务角色信息托管在 LDAP 目录中。您还详细了解了配置 Acegi 的方法,从而与 LDAP 目录交互实现访问控制策略。在本系列最后一期文章中,我将展示如何配置 Acegi 来保护对 Java 类的访问。


下载

描述名字大小
本文源代码j-acegi2.zip10KB

参考资料

学习

获得产品和技术

  • ApacheDS:从 Apache.org 下载 ApacheDS。
  • JXplorer:本文广泛使用的基于 Java 的开源 LDAP 客户机。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=231419
ArticleTitle=使用 Acegi 保护 Java 应用程序,第 2 部分: 使用 LDAP 目录服务器
publish-date=06212007