第 1 部分: 架构概览和安全过滤器

Acegi Security System 是一种功能强大并易于使用的替代性方案,使您不必再为 Java 企业应用程序编写大量的安全代码。虽然它专门针对使用 Spring 框架编写的应用程序,但是任何类型的 Java 应用程序都没有理由不去使用 Acegi。这份共分三部分的系列文章详细介绍了 Acegi,并展示了如何使用它保护简单的企业应用程序以及更复杂的应用程序。

本系列首先介绍企业应用程序中常见的安全问题,并说明 Acegi 如何解决这些问题。您将了解 Acegi 的架构模型及其安全过滤器,后者包含了在保护应用程序中将用到的大多数功能。您还将了解到各个过滤器如何单独进行工作,如何将它们组合起来,以及过滤器如何在一个企业安全实现中将各种功能从头到尾地链接起来。本文最后通过一个样例应用程序演示了基于 URL 安全系统的 Acegi 实现。本系列后续两篇文章将探究 Acegi 的一些更高级的应用,包括如何设计和托管访问控制策略,然后如何去配置 Acegi 以使用这些策略。

您必须 下载 Acegi,这样才能编译本文的示例代码并运行本文的样例应用程序。还必须有作为工作站的一部分运行的 Tomcat 服务器。

企业应用程序安全性

由于企业内容管理(ECM)应用程序管理存储在不同类型数据源(如文件系统、关系数据库和目录服务)中的企业内容的编写和处理,ECM 安全性要求对这些数据源的访问进行控制。比如,一个 ECM 应用程序可能会控制被授权读取、编辑或删除数据的对象,而这些数据和制造业企业的设计、市场营销、生产以及质量控制有关。

在一个 ECM 安全场景中,比较常见的就是通过对企业资源定位符(或网络地址)应用安全性,从而实现访问控制。这种简单的安全模型被称为统一资源定位符 或 URL 安全性。正如我在本文后面(以及本系列后续文章)所演示的一样,Acegi 为实现 URL 安全性提供了全面的特性。

然而,在很多企业场景中,URL 安全性还远远不够。比如,假设一个 PDF 文档包含某个制造业公司生产的特殊产品的数据。文档的一部分包含了将由该公司的设计部门编辑和更新的设计数据。另一部分包含了生产经理将使用的生产数据。对于诸如此类的场景,需要实现更加细粒度的安全性,对文档的不同部分应用不同的访问权限。

本文介绍了 Acegi 为实现 URL 安全性而提供的各种功能。本系列的下一篇文章将演示此框架的基于方法的安全性,它提供了对企业数据访问的更细粒度的控制。

 

回页首

Acegi Security System

Acegi Security System 使用安全过滤器来提供企业应用程序的身份验证和授权服务。该框架提供了不同类型的过滤器,可以根据应用程序的需求进行配置。您将在本文后面了解到 安全过滤器的不同类型;现在,只需注意可以为如下任务配置 Acegi 安全过滤器:

  1. 在访问一个安全资源之前提示用户登录。
  2. 通过检查安全标记(如密码),对用户进行身份验证。
  3. 检查经过身份验证的用户是否具有访问某个安全资源的特权。
  4. 将成功进行身份验证和授权的用户重定向到所请求的安全资源。
  5. 对不具备访问安全资源特权的用户显示 Access Denied 页面。
  6. 在服务器上记录成功进行身份验证的用户,并在用户的客户机上设置安全 cookie。使用该 cookie 执行下一次身份验证,而无需要求用户登录。
  7. 将身份验证信息存储在服务器端的会话对象中,从而安全地进行对资源的后续请求。
  8. 在服务器端对象中构建并保存安全信息的缓存,从而优化性能。
  9. 当用户退出时,删除为用户安全会话而保存的服务器端对象。
  10. 与大量后端数据存储服务(如目录服务或关系数据库)进行通信,这些服务用于存储用户的安全信息和 ECM 的访问控制策略。

正如这个列表显示的那样,Acegi 的安全过滤器允许您执行保护企业应用程序所需的几乎任何事情。

 

回页首

架构和组件

对 Acegi 了解越多,使用起来就越简单。这一节介绍 Acegi 的组件;接下来您将了解该框架如何使用反转控制(IOC)和 XML 配置文件来组合组件并表示它们的依赖关系。

四大组件

Acegi Security System 由四种主要类型的组件组成:过滤器、管理器、提供者和处理程序。

过滤器
这种最高级的组件提供了常见的安全服务,如身份验证、会话处理以及注销。我将在 本文后面的部分 深入讨论过滤器。
管理器
过滤器仅是安全相关功能的高级抽象:实际上要使用管理器和提供者实现身份验证处理和注销服务。管理器管理由不同提供者提供的较低级的安全服务。
提供者
有大量的提供者可用于和不同类型的数据存储服务通信,例如目录服务、关系数据库或简单的内存中的对象。这意味着您可以将用户库和访问控制协议存储在任何一种这样的数据存储服务中,并且 Acegi 的管理器将在运行时选择合适的提供者。
处理程序
有时任务可能会被分解为多个步骤,其中每个步骤由一个特定的处理程序执行。比方说,Acegi 的 注销过滤器 使用两个处理程序来退出一个 HTTP 客户机。其中一个处理程序使用户的 HTTP 会话无效,而另一个处理程序则删除用户的 cookie。当根据应用程序需求配置 Acegi 时,多个处理程序能够提供很好的灵活性。您可以使用自己选择的处理程序来执行保护应用程序所需的步骤。

反转控制

Acegi 的组件通过彼此之间的依赖来对企业应用程序进行保护。比如,一个身份验证处理过滤器需要一个身份验证管理器选择一个合适的身份验证提供者。这就是说您必须能够表示和管理 Acegi 组件的依赖关系。

IOC 实现通常用于管理 Java 组件之间的依赖关系。IOC 提供了两个重要的特性:

  1. 它提供了一种语法,表示应用程序所需的组件以及这些组件如何相互依赖。
  2. 它保证了所需的组件在运行时是可用的。

XML 配置文件

Acegi 使用 Spring 框架(请参见 参考资料)附带的流行开源 IOC 实现来管理其组件。Spring 需要您编写一个 XML 配置文件来表示组件的依赖关系,如清单 1 所示:

清单 1. Spring 配置文件的结构
<beans>
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value> value here </value>
</property>
</bean> <bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFitler">
<property name="authenticationManager" ref="authManager"/>
<!-- Other properties -->
</bean> <bean id="authManager"
class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<!-- List of providers here -->
</property>
</bean>
<!-- Other bean tags -->
</beans>

如您所见,Acegi 使用的 Spring XML 配置文件包含一个 <beans> 标记,它封装了一些其他的 <bean> 标记。所有的 Acegi 组件(即过滤器、管理器、提供者等)实际上都是 JavaBean。XML 配置文件中的每个 <bean> 标记都代表一个 Acegi 组件。

进一步解释 XML 配置文件

首先将注意到的是每个 <bean> 标记都包含一个 class 属性,这个属性标识组件所使用的类。<bean> 标记还具有一个 id 属性,它标识作为 Acegi 组件工作的实例(Java 对象)。

比方说,清单 1 的第一个 <bean> 标记标识了名为 filterChainProxy 的组件实例,它是名为 org.acegisecurity.util.FilterChainProxy的类的实例。

使用 <bean> 标记的子标记来表示 bean 的依赖关系。比如,注意第一个 <bean> 标记的 <property> 子标记。<property> 子标记定义了<bean> 标记依赖的其他 bean 或值。

所以在 清单 1 中,第一个 <bean> 标记的 <property> 子标记具有一个 name 属性和一个 <value> 子标记,分别定义了这个 bean 依赖的属性的名称和值。

同样,清单 1 中的第二个和第三个 <bean> 标记定义了一个过滤器 bean 依赖于一个管理器 bean。第二个 <bean> 标记表示过滤器 bean,而第三个 <bean> 标记表示管理器 bean。

过滤器的 <bean> 标记包含一个 <property> 子标记,该子标记具有两个属性:name 和 refname 属性定义了过滤器 bean 的属性,而 ref 属性引用了管理器 bean 的实例(名称)。

下一节将展示如何在 XML 配置文件中配置 Acegi 过滤器。在本文后面的内容中,您将在一个样例 Acegi 应用程序中使用过滤器。

 

回页首

安全过滤器

正如我前面提到的一样,Acegi 使用安全过滤器为企业应用程序提供身份验证和授权服务。您可以根据应用程序的需要使用和配置不同类型的过滤器。这一节将介绍五种最重要的 Acegi 安全过滤器。

Session Integration Filter

Acegi 的 Session Integration Filter(SIF)通常是您将要配置的第一个过滤器。SIF 创建了一个安全上下文对象,这是一个与安全相关的信息的占位符。其他 Acegi 过滤器将安全信息保存在安全上下文中,也会使用安全上下文中可用的安全信息。

SIF 创建安全上下文并调用过滤器链中的其他过滤器。然后其他过滤器检索安全上下文并对其进行更改。比如,Authentication Processing Filter(我将稍后进行介绍)将用户信息(如用户名、密码和电子邮件地址)存储在安全上下文中。

当所有的处理程序完成处理后,SIF 检查安全上下文是否更新。如果任何一个过滤器对安全上下文做出了更改,SIF 将把更改保存到服务器端的会话对象中。如果安全上下文中没有发现任何更改,那么 SIF 将删除它。

在 XML 配置文件中对 SIF 进行了配置,如清单 2 所示:

清单 2. 配置 SIF
<bean id="httpSessionContextIntegrationFilter"
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/>

Authentication Processing Filter

Acegi 使用 Authentication Processing Filter(APF)进行身份验证。APF 使用一个身份验证(或登录)表单,用户在其中输入用户名和密码,并触发身份验证。

APF 执行所有的后端身份验证处理任务,比如从客户机请求中提取用户名和密码,从后端用户库中读取用户参数,以及使用这些信息对用户进行身份验证。

在配置 APF 时,您必须提供如下参数:

  • Authentication manager 指定了用来管理身份验证提供者的身份验证管理器。
  • Filter processes URL 指定了客户在登录窗口中按下 Sign In 按钮时要访问的 URL。收到这个 URL 的请求后,Acegi 立即调用 APF。
  • Default target URL 指定了成功进行身份验证和授权后呈现给用户的页面。
  • Authentication failure URL 指定了身份验证失败情况下用户看到的页面。

APF 从用户的请求对象中得到用户名、密码和其他信息。它将这些信息传送给身份验证管理器。身份验证管理器使用适当的提供者从后端用户库中读取详细的用户信息(如用户名、密码、电子邮件地址和用户访问权利或特权),对用户进行身份验证,并将信息存储在一个Authentication 对象中。

最后,APF 将 Authentication 对象保存在 SIF 之前创建的安全上下文中。存储在安全上下文中的 Authentication 对象将用于做出授权决策。

APF 的配置如清单 3 所示:

清单 3. 配置 APF
<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager"
ref="authenticationManager" />
<property name="filterProcessesUrl"
value="/j_acegi_security_check" />
<property name="defaultTargetUrl"
value="/protected/protected1.jsp" />
<property name="authenticationFailureUrl"
value="/login.jsp?login_error=1" />
</bean>

可以从这段代码中看到,APF 依赖于上面讨论的这四个参数。每个参数都是作为清单 3 所示的 <property> 标记配置的。

Logout Processing Filter

Acegi 使用一个 Logout Processing Filer(LPF)管理注销处理。当客户机发来注销请求时,将使用 LPF 进行处理。它标识了来自由客户机所调用 URL 的注销请求。

LPF 的配置如清单 4 所示:

清单 4. 配置 LPF
<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="/logoutSuccess.jsp"/>
<constructor-arg>
<list>
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
</list>
</constructor-arg>
</bean>

可以看到 LPF 在其构造方法中包含两个参数:注销成功 URL(/logoutSuccess.jsp)和处理程序列表。注销成功 URL 用来在注销过程完成后重定向客户机。处理程序执行实际的注销过程;我在这里只配置了一个处理程序,因为只需一个处理程序就可以使 HTTP 会话变为无效。我将在本系列下一篇文章中讨论更多的处理程序。

Exception Translation Filter

Exception Translation Filter(ETF)处理身份验证和授权过程中的异常情况,比如授权失败。在这些异常情况中,ETF 将决定如何进行操作。

比如,如果一个没有进行身份验证的用户试图访问受保护的资源,ETF 将显示一个登录页面要求用户进行身份验证。类似地,在授权失败的情况下,可以配置 ETF 来呈现一个 Access Denied 页面。

ETF 的配置如清单 5 所示:

清单 5. 配置 ETF
<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.jsp" />
</bean>
</property>
<property name="accessDeniedHandler">
<bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.jsp" />
</bean>
</property>
</bean>

正如清单 5 所示,ETF 包含两个参数,名为 authenticationEntryPoint 和 accessDeniedHandlerauthenticationEntryPoint 属性指定登录页面,而 accessDeniedHandler 指定 Access Denied 页面。

拦截过滤器

Acegi 的拦截过滤器 用于做出授权决策。您需要在 APF 成功执行身份验证后对拦截过滤器进行配置,以使其发挥作用。拦截器使用应用程序的访问控制策略来做出授权决定。

本系列的下一篇文章将展示如何设计访问控制策略,如何将它们托管在目录服务中,以及如何配置 Acegi 以读取您的访问控制策略。但是,目前我将继续向您展示如何使用 Acegi 配置一个简单的访问控制策略。在本文后面的部分,您将看到使用简单的访问控制策略构建一个样例应用程序。

配置简单的访问控制策略可分为两个步骤:

  1. 编写访问控制策略。
  2. 根据策略配置 Acegi 的拦截过滤器。

步骤 1. 编写简单的访问控制策略

首先看一下 清单 6,它展示了如何定义一个用户及其用户角色:

清单 6. 为用户定义简单的访问控制策略
    alice=123,ROLE_HEAD_OF_ENGINEERING

清单 6 所示的访问控制策略定义了用户名 alice,它的密码是 123,角色是 ROLE_HEAD_OF_ENGINEERING。(下一节将说明如何在文件中定义任意数量的用户及其用户角色,然后配置拦截过滤器以使用这些文件。)

步骤 2. 配置 Acegi 的拦截过滤器

拦截过滤器使用三个组件来做出授权决策,我在清单 7 中对其进行了配置:

清单 7. 配置拦截过滤器
<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/**=ROLE_HEAD_OF_ENGINEERING
/**=IS_AUTHENTICATED_ANONYMOUSLY
</value>
</property>
<!-- More properties of the interceptor filter -->
</bean>

如清单 7 所示,配置所需的三个组件是 authenticationManageraccessDecisionManagerobjectDefinitionSource

  • authenticationManager 组件与我在介绍 Authentication Processing Filter 时讨论过的身份验证管理器相同。拦截过滤器可以在授权的过程中使用 authenticationManager 重新对客户机进行身份验证。
  • accessDecisionManager 组件管理授权过程,这部分内容将在本系列的下篇文章中详细讨论。
  • objectDefinitionSource 组件包含对应于将要发生的授权的访问控制定义。例如,清单 7 中的 objectDefinitionSource 属性值包含两个 URL(/protected/* 和 /*)。其值定义了这些 URL 的角色。/protected/* URL 的角色是 ROLE_HEAD_OF_ENGINEERING。您可以根据应用程序的需要定义任何角色。

    回想一下 清单 6,您为用户名 alice 定义了 ROLE_HEAD_OF_ENGINEERING。这就是说 alice 将能够访问 /protected/* URL。

 

回页首

过滤器工作原理

正如您已经了解到的一样,Acegi 的组件彼此依赖,从而对您的应用程序进行保护。在本文后面的部分,您将看到如何对 Acegi 进行配置,从而按照特定的顺序应用安全过滤器,因此需要创建过滤器链。出于这个目的,Acegi 保存了一个过滤器链对象,它封装了为保护应用程序而配置了的所有过滤器。图 1 展示了 Acegi 过滤器链的生命周期,该周期从客户机向您的应用程序发送 HTTP 请求开始。(图 1 显示了服务于浏览器客户机的容器。)

图 1. 托管 Acegi 过滤器链以安全地为浏览器客户机服务的容器

使用 Acegi 保护 Java 应用程序-LMLPHP

下面的步骤描述了过滤器链的生命周期:

  1. 浏览器客户机向您的应用程序发送 HTTP 请求。
  2. 容器接收到 HTTP 请求并创建一个请求对象,该对象将封装 HTTP 请求中包含的信息。容器还创建一个各种过滤器都可处理的响应对象,从而为发出请求的客户机准备好 HTTP 响应。容器然后调用 Acegi 的过滤器链代理,这是一个代理过滤器。该代理知道应用的过滤器的实际顺序。当容器调用代理时,它将向代理发送请求、响应以及过滤器链对象。
  3. 代理过滤器调用过滤器链中第一个过滤器,向其发送请求、响应和过滤器链对象。
  4. 链中的过滤器逐个执行其处理。一个过滤器可以通过调用过滤器链中下一个过滤器随时终止自身处理。有的过滤器甚至根本不执行任何处理(比如,如果 APF 发现一个到来的请求没有要求身份验证,它可能会立即终止其处理)。
  5. 当身份验证过滤器完成其处理时,这些过滤器将把请求和响应对象发送到应用程序中配置的拦截过滤器。
  6. 拦截器决定是否对发出请求的客户机进行授权,使它访问所请求的资源。
  7. 拦截器将控制权传输给应用程序(比如,成功进行了身份验证和授权的客户机请求的 JSP 页面)。
  8. 应用程序改写响应对象的内容。
  9. 响应对象已经准备好了,容器将响应对象转换为 HTTP 响应,并将响应发送到发出请求的客户机。

为帮助您进一步理解 Acegi 过滤器,我将详细探讨其中两个过滤器的操作:Session Integration Filter 和 Authentication Processing Filter。

SIF 如何创建一个安全上下文

图 2 展示了 SIF 创建安全上下文所涉及到的步骤:

图 2. SIF 创建安全上下文

使用 Acegi 保护 Java 应用程序-LMLPHP

现在详细地考虑下面这些步骤:

  1. Acegi 的过滤器链代理调用 SIF 并向其发送请求、响应和过滤器链对象。注意:通常将 SIF 配置为过滤器链中第一个过滤器。
  2. SIF 检查它是否已经对这个 Web 请求进行过处理。如果是的话,它将不再进一步进行处理,并将控制权传输给过滤器链中的下一个过滤器(参见下面的第 4 个步骤)。如果 SIF 发现这是第一次对这个 Web 请求调用 SIF,它将设置一个标记,将在下一次使用该标记,以表示曾经调用过 SIF。
  3. SIF 将检查是否存在一个会话对象,以及它是否包含安全上下文。它从会话对象中检索安全上下文,并将其放置在名为 security context holder 的临时占位符中。如果不存在会话对象,SIF 将创建一个新的安全上下文,并将它放到 security context holder 中。注意:security context holder 位于应用程序的范围内,所以可以被其他的安全过滤器访问。
  4. SIF 调用过滤器链中的下一个过滤器。
  5. 其他过滤器可以编辑安全上下文。
  6. SIF 在过滤器链完成处理后接收控制权。
  7. SIF 检查其他的过滤器是否在其处理过程中更改了安全上下文(比如,APF 可能将用户详细信息存储在安全上下文中)。如果是的话,它将更新会话对象中的安全上下文。就是说在过滤器链处理过程中,对安全上下文的任何更改现在都保存在会话对象中。

APF 如何对用户进行身份验证

图 3 展示了 APF 对用户进行身份验证所涉及到的步骤:

图 3. APF 对用户进行身份验证

使用 Acegi 保护 Java 应用程序-LMLPHP

现在仔细考虑以下这些步骤:

  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. 如果缓存返回 null,身份验证提供者将用户名(在步骤 6 中提取)发送给另一个名为 user details service 的服务。
  11. user details service 与包含用户详细信息的后端数据存储通信(如目录服务)。
  12. user details service 返回用户的详细信息,或者,如果找不到用户详细信息则抛出身份验证异常。
  13. 如果 user cache service 或者 user details service 返回有效的用户详细信息,身份验证提供者将使用 user cache service 或 user details service 返回的密码来匹配用户提供的安全标记(如密码)。如果找到一个匹配,身份验证提供者将用户的详细信息返回给身份验证管理器。否则的话,则抛出一个身份验证异常。
  14. 身份验证管理器将用户的详细信息返回给 APF。这样用户就成功地进行了身份验证。
  15. APF 将用户详细信息保存在 图 2 所示由步骤 3 创建的安全上下文中。
  16. APF 将控制权传输给过滤器链中的下一个过滤器。
 

回页首

一个简单的 Acegi 应用程序

在本文中,您已经了解了很多关于 Acegi 的知识,所以现在看一下利用您目前学到的知识能做些什么,从而结束本文。对于这个简单的演示,我设计了一个样例应用程序(参见 下载),并对 Acegi 进行了配置以保护它的一些资源。

样例应用程序包含 5 个 JSP 页面:index.jsp、protected1.jsp、protected2.jsp、login.jsp 和 accessDenied.jsp。

index.jsp 是应用程序的欢迎页面。它向用户显示了三个超链接,如图 4 所示:

图 4. 样例应用程序的欢迎页面:

使用 Acegi 保护 Java 应用程序-LMLPHP

图 4 所示的链接中,其中两个链接指向了被保护的资源(protected1.jsp 和 protected2.jsp),第三个链接指向登录页面(login.jsp)。只有在 Acegi 发现用户没有被授权访问受保护的资源时,才会显示 accessDenied.jsp 页面。

如果用户试图访问任何受保护的页面,样例应用程序将显示登录页面。当用户使用登录页面进入后,应用程序将自动重定向到被请求的受保护资源。

用户可以通过单击欢迎页面中的第三个链接直接请求登录页面。这种情况下,应用程序显示用户可以进入系统的登录页面。进入系统以后,应用程序将用户重定向到 protected1.jsp,它是用户进入系统而没有请求特定的受保护资源时显示的默认资源。

配置样例应用程序

为本文下载的源代码包含一个名为 acegi-config.xml 的 XML 配置文件,它包含 Acegi 过滤器的配置。根据 安全过滤器的讨论 中的示例,您应该很熟悉这些配置。

我还为样例应用程序编写了一个 web.xml 文件,如清单 8 所示:

清单 8. 样例应用程序的 web.xml 文件
<web-app>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/acegi-config.xml</param-value>
</context-param>
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>
org.acegisecurity.util.FilterToBeanProxy
</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>
org.acegisecurity.util.FilterChainProxy
</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
</web-app>

web.xml 文件配置如下:

  • acegi-config.xml 文件的 URL 位于 <context-param> 标记中。
  • Acegi 过滤器链代理类的名称位于 <filter> 标记中。
  • URL 到 Acegi 过滤器链代理的映射在 <filter-mapping> 标记中。注意:您可以简单地将应用程序的所有 URL(/*)映射到 Acegi 过滤器链代理。Acegi 将对映射到 Acegi 过滤器链代理上的所有 URL 应用安全性。
  • 应用程序上下文加载程序位于 <listener> 标记中,它将加载 Spring 的 IOC 框架。

部署并运行应用程序

部署并运行样例应用程序非常的简单。只需要完成两件事情:

  1. 将 acegisample.war 文件从本教程下载的源代码中复制到安装 Tomcat 的 webapps 目录中。
  2. 从 Acegi Security System 主页 下载并解压缩 acegi-security-1.0.3.zip。您将找到一个名为 acegi-security-sample-tutorial.war 的样例应用程序。解压缩 war 文件并提取其 WEB-INF/lib 文件夹中所有的 jar 文件。将所有的 JAR 文件从 WEB-INF/lib 文件夹中复制到 theacegisample.war 应用程序的 WEB-INF/lib 文件夹。

现在,您已经为运行样例应用程序做好准备了。启动 Tomcat 并将浏览器指向 http://localhost:8080/acegisample/

您将看到 图 4 所示的欢迎页面,但是此时显示的页面是真实的。请继续运行程序,并查看在尝试访问欢迎页面显示的不同链接时会发生什么状况。

 

回页首

结束语

使用 Acegi 保护 Java 应用程序 系列的第一篇文章中,您了解了 Acegi 安全系统的特性、架构和组件,学习了大量有关 Acegi 安全过滤器的知识,这些过滤器被集成到 Acegi 的安全框架中。您还学习了如何使用 XML 配置文件配置组件依赖关系,并查看了 Acegi 的安全过滤器在样例程序中工作的情形,该应用程序可以实现基于 URL 的安全性。

本文所述的安全技术非常的简单,所以 Acegi 使用这些技术实现安全性。本系列的下一文章将开始介绍 Acegi 的一些较为高级的应用,首先是编写访问控制协议并将其存储到目录服务中。您还将了解到如何配置 Acegi,使它与目录服务交互,从而实现您的访问控制策略。

第 2 部分: 使用 LDAP 目录服务器

这期共分三部分的系列文章介绍了如何使用 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 目录的树状结构

使用 Acegi 保护 Java 应用程序-LMLPHP

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

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

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

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

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

使用专有名称

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

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

使用 Acegi 保护 Java 应用程序-LMLPHP

首先,注意图 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 根节点使用两个对象类:top 和 domain

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

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

图 3. JXplorer 显示根节点

使用 Acegi 保护 Java 应用程序-LMLPHP

填充服务器

设置 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

使用 Acegi 保护 Java 应用程序-LMLPHP

将 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 属性。构造函数的第一个参数指定了 populatorbean 用来读取经过验证用户的业务角色的初始上下文。并不强制要求 authenticator 和 populator 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 和密码分别定义为 managerDn 和 managerPassword 属性。在执行任何搜索操作之前,Acegi 必须使用目录服务器对 DN 和密码进行身份验证。

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

 

回页首

身份验证和授权

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

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

使用 Acegi 保护 Java 应用程序-LMLPHP

无论 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 将使用在 清单 5 的 userDnPatterns 属性中配置的 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 对象传递给投票者。本文的示例使用了两种类型的投票者,RoleVoter 和 AuthenticatedVoter,如清单 10 所示。现在看一下每种投票者的逻辑:

  • RoleVoter 只有在 objectDefinitionSource 对象的行中找到以 ROLE_ 前缀开头的角色时才进行投票。如果 RoleVoter 没有找到这样的行,将放弃投票;如果在用户业务角色中找到一个匹配的角色,它将投票给允许访问;如果没有找到匹配的角色,则投票给拒绝访问。在 清单 9 中,有两个角色具有 ROLE_ 前缀:ROLE_HEAD_OF_ENGINEERING 和 ROLE_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 类的访问。

第 3 部分: 实现对 Java 对象的访问控制

这期共分三部分的系列文章介绍了如何使用 Acegi 安全系统保护 Java 企业应用程序。系列文章的 第 1 部分 简单介绍了 Acegi 并解释如何使用其内置的安全过滤器实现一个简单的、基于 URL 的安全系统。第 2 部分 介绍了如何编写访问控制策略并将其保存到一个 LDAP 目录服务器,以及如何配置 Acegi 来与目录服务器进行交互,从而实现访问控制策略。第 3 部分(也是本系列的最后一篇文章)将演示如何在企业应用程序中使用 Acegi 保护对 Java 类实例的访问。

首先我将介绍何时需要对 Java 类访问进行保护,包括文中引用的两个典型企业应用程序场景。之后,我将解释 Spring 的反转控制(IOC)框架如何创建可从 JSP 或 servlet 访问的 Java 类实例。我还将介绍有关 bean 代理 的重要概念,Spring 正是使用它过滤对 Java 类的访问。最后,我将介绍如何对 Acegi 的方法安全性拦截器进行配置以控制对 Java 类的访问。我将对 第 2 部分 中的示例程序进行增强,为实现安全的 Java 对象提供支持,从而结束本系列的最后一篇文章。

由于本文的讨论构建在本系列前两部分的内容之上,因此会经常引用到 第 1 部分 和 第 2 部分 中的讨论和示例。因此,在继续阅读本文之前,在其他浏览器窗口中打开前两期文章将有助于理解本文内容。

保护 Java 类的用例

您可能还记得,我曾在本系列的开头部分简单介绍了 企业应用程序安全性。在那次讨论中我曾提到过一种场景,其中 URL 安全性并不能完全满足这种场景的安全需求:

在继续阅读之前,请考虑更多的应用程序场景,除了实现 URL 安全性以外,这些场景还要求您对单独的类访问进行保护。

业务自动化

业务自动化应用程序中的工作流由多个流程组成。例如,病理学实验室中执行血液测试的工作流由若干个步骤组成,其中每个步骤可看作一个业务流程:

  1. 工作人员从病人处采集血液样本并为其分配一个 ID。
  2. 实验室技术人员对样本进行必要的测试并准备测试结果。
  3. 由具备相应资格的病理学专家根据测试结果编写测试报告。

很明显,每个流程分别由单独的授权用户执行。未授权的用户则无权执行流程。例如,实验室研究人员只负责准备试验结果,而无权编写测试报告。

几乎所有的业务自动化应用程序都普遍使用授权的业务流程。通常,每个业务流程被实现为一个 Java 类,并且需要使用合适的访问控制策略对所有类实施保护。

企业对企业(Business-to-business)集成

Business-to-business (B2B) 集成指一种常见的场景,其中的两个企业实体需要彼此公开各自的特定功能。例如,宾馆可能向旅游公司公开其房间预订功能,而后者使用该功能为游客预订空闲的房间。作为合作伙伴的旅游公司可能具有一个特定的订房率。在这个场景中,宾馆的订房系统必须先对旅游公司进行身份验证,然后才能允许他们访问所选择的类,以便按照特定的订房率进行房间预订。

 

回页首

使用 Spring 创建 Java 对象

现在您已经了解了对 Java 类示例的访问进行保护的重要性。在介绍能够实现更高级安全性的 Acegi 新功能之前,我将引导您回顾 Spring 框架的几个关键特性,您需要了解这些内容才能继续后文的示例。

首先对一些 Java 类进行配置并执行实例化。第 1 部分 曾介绍过,Java 类在 Spring 的 XML 配置文件中进行配置。在 Spring 配置文件中配置 Java 类的过程与 Acegi 过滤器的配置过程完全相同,因此这里不多做介绍。相反,我们将查看清单 1,它展示了名为 publicCatalog 的 bean 的配置:

清单 1. Acegi XML 配置文件
<beans>
<bean id="publicCatalog"
class="com.catalog.PublicCatalog" />
<!--Other bean tags -->
<beans>

了解 Spring 的 IOC 框架如何从 XML 配置文件读取 Java 类信息以及如何进行实例化,这一点非常重要。您可能还记得,我在系列文章的 第 1 部分 中使用一个 web.xml 文件配置 <listener> 标记,它指向名为 ContextLoaderListener 的类。ContextLoaderListener 装载 Spring 的 IOC 框架并创建 Java 对象。您可以参考 第 1 部分的清单 8 查看全部内容。图 1 也对此进行了描述:

图 1. 装载 Spring 的 IOC 框架并创建 Java 对象

使用 Acegi 保护 Java 应用程序-LMLPHP

现在我们将详细讨论这些步骤:

  1. 当初始化 Acegi 应用程序时,servlet 容器(本例中为 Apache Tomcat)创建了一个 servlet 上下文,其中保存了有关应用程序资源的信息,例如 JSP 页面和类。
  2. servlet 容器通知 ContextLoaderListener 类应用程序正在启动。
  3. ContextLoaderListener 类创建一个 Web 应用程序上下文以保存应用程序中特定于 Spring 的资源信息。借助 Spring 的 IOC 框架,您可以装载自己的自定义应用程序上下文。要创建应用程序上下文,将使用名为 ContextLoader 的上下文装载器类装载应用程序上下文。
  4. 如果应用程序不需要定义自己的应用程序上下文,则可以使用名为 XMLWebApplicationContext 的类,它是 Spring 框架的一部分并提供可处理 Spring XML 配置文件的功能。Acegi 应用程序使用的是 Spring 的 XML 配置文件,因此本文仅讨论由 XMLWebApplicationContext 类表示的应用程序上下文。在本例中,上下文装载器对 XMLWebApplicationContext 类进行实例化,后者表示您的 Acegi 应用程序的应用程序上下文。上下文装载器还在 Web 应用程序上下文中设置 servlet 上下文(于步骤 1 中创建)的引用。
  5. XMLWebApplicationContext 类对 XML 配置文件进行解析,获得关于 Java 类的信息并将信息装载到其他内部对象中。
  6. XMLWebApplicationContext 类对 XML 配置文件中指定的所有 Java 类进行实例化。XMLWebApplicationContext 类检查 XML 配置文件中经过配置的 Java bean 是否依赖其他的 Java 对象。如果是的话,XMLWebApplicationContext 类将首先对其他 bean 所依赖的 bean 进行实例化。通过这种方式,XMLWebApplicationContext 类创建了 XML 配置文件中定义的所有 bean 的实例。(注意,步骤 6 假定 XML 配置文件中所有 bean 都不要进行保护,稍后一节将介绍步骤 5 和步骤 6 之间执行的额外步骤,从而保护对此处创建的 Java bean 的访问)。
  7. XMLWebApplicationContext 类将所有 bean 保存在一个数组中。

您现在已了解到如何从 XML 配置文件中装载 bean 定义并创建 Java 类的实例。接下来,我将向您介绍 Spring bean 代理并解释它对于保护 Java 类实例的重要性。

 

回页首

使用 bean 代理

上一节讨论了 Spring 的 IOC 框架对 Java 对象进行实例化。要保护对 Java 对象的访问,Spring 的 IOC 框架使用了 bean 代理 的概念。本节首先介绍如何配置 bean 代理,然后演示 Spring 的 IOC 框架如何创建代理对象。

为 Java 对象配置代理

如果希望创建 bean 代理,Spring IOC 框架要求您对代理创建器 bean 的实例进行配置。Spring 的 IOC 框架使用代理创建器创建代理对象。清单 2 为代理创建器 bean 的配置文件,用于保护名为 privateCatalog 的 Java 对象:

清单 2. 代理 bean 配置
<bean id="proxyCreator"
class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>privateCatalog</value>
<!--Names of other beans to be proxied -->
</list>
</property>
<property name="interceptorNames">
<list>
<value>privateCatalogSecurityInterceptor</value>
</list>
</property>
</bean>

如清单 2 所示,<bean> 标记具有一个 class 属性,其值为 org.springframework.aop.framework.autoproxy. BeanNameAutoProxyCreatorBeanNameAutoProxyCreator 类是 Spring IOC 框架的一部分,可以自动创建 bean 代理。Spring 框架提供了BeanPostProcessor 接口,它提供了一种可扩展机制,允许应用程序编写自己的逻辑来创建 bean 代理。Spring 的BeanNameAutoProxyCreator 类实现了 BeanPostProcessor 接口并提供所有必需的代理创建逻辑来保护 Java 类。因此,本文中您无需实现BeanPostProcessor 接口。

在创建 bean 代理时,BeanNameAutoProxyCreator 类为所有由 beanNames 属性定义的 bean 创建代理(参见 清单 2 中 <bean> 标记的第一个<property> 子元素)。beanNames 属性在 <list> 标记中包含一个 bean 名称列表。在 清单 2 中,我只对希望为之创建代理的privateCatalog bean进行了配置。

现在查看 清单 2 中 <bean> 标记的第二个 <property> 子元素。它指定了名为 interceptorNames 的代理,它将一个或多个拦截器的名称封装起来。我将在后文详细讨论拦截器概念。现在,只需了解拦截器可以拦截用户并在用户访问 bean 之前实现访问控制策略。

现在,您已了解了如何对希望进行保护的 bean 配置代理。接下来,您将了解 Spring 的 IOC 框架如何在内部为应用程序的 bean 创建代理对象。

Spring IOC 发挥效用

在 “使用 Spring 创建 Java 对象” 的步骤 5 和步骤 6 中,您了解了 XMLWebApplicationContext 类如何从 XML 配置文件中读取 bean 定义并随后创建 bean 实例。在创建 bean 实例之前,XMLWebApplicationContext 类将检查 XML 配置文件是否包含任何代理创建器 bean(即实现BeanPostProcessor 接口的 bean)配置。如果存在该 bean,它将要求代理创建器为您希望进行保护的 bean 创建 bean 代理。

现在考虑代理创建器如何在内部创建代理对象:

  1. 代理创建器(即 BeanNameAutoProxyCreator 类)装载 清单 2 中配置的 beanNames 属性文件中指定的所有 bean 名称。
  2. 代理创建器使用 bean 名称装载各自的 Java 类,这些类使用了每个 bean 定义的 class 属性。
  3. 代理创建器创建 清单 2 所示的 interceptorNames 属性中指定的拦截器的实例。
  4. 最后,代理创建器创建一个 Cglib2AopProxy 类的实例,将所有 bean 名称(步骤 2)和拦截器(步骤 3)传递到 Cglib2AopProxy类。Cglib2AopProxy 类是 Spring 框架的一部分并用于生成动态代理对象。在本例中,Cglib2AopProxy 类将创建安全 bean 访问控制所需的代理对象。

Cglib2AopProxy 类实现了两个名为 AOPProxy 和 MethodInterceptor 的接口。AOPProxy 接口由 Spring 框架提供,表示您希望进行代理的实际 bean,因此它与您的 bean 公开相同的方法。MethodInterceptor 接口也源于 AOP 框架,它包含的方法可以在用户试图访问您已执行代理的 bean 时接受控制权。这意味着 MethodInterceptor 接口处理来自用户的请求以访问执行过代理的 bean。由于 Cglib2AopProxy 类同时实现了 AOPProxy 和 MethodInterceptor 接口,因此它提供了完整的功能,既可以提供经过代理的 bean,也可以处理用户请求以访问代理 bean(参见 参考资料小节 中有关 AOP 的讨论文章的链接)。

执行完前面的步骤后,您现在具有了所需的代理对象。因此 XMLWebApplicationContext 类将安全 bean 的代理(而不是实际的 bean)保存在 “使用 Spring 创建 Java 对象” 的步骤 7 中的同一个数组中。

 

回页首

访问执行过代理的 Java 对象

在前面的几节中,您了解了 Spring 如何创建公有 bean 和私有 bean。出于本文的目的,您可将公有 bean 视为使用代理保护的不安全的私有 bean。现在我们来看一下客户机应用程序为访问公有 bean 和私有 bean 而必须遵循的一系列步骤。

清单 3 展示了 publicCatalog 和 privateCatalog 两个 bean 的 XML 配置。publicCatalog bean 意味着公共访问,因此不需要使用 bean 代理。privateCatalog bean 意味着只能由指定用户访问,因此必须加以保护。我在清单 3 中包含了 privateCatalog bean 的 bean 代理配置:

清单 3. publicCatalog 和 privateCatalog bean 的 XML 配置
<beans>
<bean id="publicCatalog" class="sample.PublicCatalog"/>
<bean id="privateCatalog" class="sample.PrivateCatalog"/> <!-- proxy configuration for privateCatalog bean -->
<bean id="proxyCreator"
class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>privateCatalog</value>
<!--Names of other beans to be proxied -->
</list>
</property>
<property name="interceptorNames">
<list>
<value>privateCatalogSecurityInterceptor</value>
</list>
</property>
</bean>
<beans>

应用程序可以使用清单 4 中的代码访问清单 3 中配置的 publicCatalog 和 privateCatalog Java bean。注意,清单 4 中显示的 Java 代码可位于 JSP 页面或位于服务器端 Java 应用程序的 bean 中。

清单 4. 访问安全和不安全 Java bean 的客户机应用程序代码
//Step 1: Fetching an instance of the application context
XMLWebApplicationContext applicationCtx =
WebApplicationContextUtils.getWebApplicationContext(
this.getServletConfig().getServletContext()); //Step 2: Fetching an insecure bean from the application context
PublicCatalog publicCatalog =
(PublicCatalog) applicationCtx.getBean("publicCatalog"); //Step 3: Calling a method of the insecure bean
String publicData = publicCatalog.getData(); //Step 4: Fetching a secure bean from the application context
PrivateCatalog privateCatalog =
(PrivateCatalog) applicationCtx.getBean("privateCatalog"); //Step 5: Calling a method of the secure bean
String privateData = privateCatalog.getData();

下面将进一步讨论清单 4 中的步骤:

  • 步骤 1:取回一个应用程序上下文实例
    当应用程序希望访问 XML 配置文件中配置的 Java bean 时,它必须取回您在 “使用 Spring 创建 Java 对象” 的步骤 4 中见到的XMLWebApplicationContext 对象。XMLWebApplicationContext 对象包含对 XML 配置文件配置的所有 Java beans 的引用。
  • 步骤 2:从应用程序上下文中取回不安全的 bean
    您现在具有一个对 XMLWebApplicationContext 对象的引用。XMLWebApplicationContext 类公开了一个 getBean() 方法,它包含 bean 的名称并在数组中查找 “使用 Spring 创建 Java 对象” 步骤 7 中准备的 bean。在本例中,该 bean 为 publicCatalog(未执行过代理),因此XMLWebApplicationContext 将返回实际的 bean。
  • 步骤 3:调用不安全 bean 的方法
    现在您可以调用步骤 2 中获得的 publicCatalog bean 的任何方法。例如,清单 4 显示的 getData() 方法调用的执行没有应用任何访问控制并向应用程序返回类别数据。
  • 步骤 4:从应用程序上下文取回安全 bean
    安全 bean 与不安全 bean 的取回方式类似,惟一区别是:当您通过调用 getBean() 方法尝试取回安全 bean 时,您将获得安全对象的代理而不是实际的对象。该代理就是我在 “Spring IOC 发挥效用” 步骤 4 中解释的由 Spring 框架创建的同一个对象。
  • 步骤 5:调用安全 bean 的方法
    当调用安全 bean 的方法时,您在 步骤 4 中获得的代理对象将一个方法调用请求分配给拦截器。拦截器将检查试图访问方法的用户是否具有相应的访问权,从而处理方法调用请求。

您现在应该对 Spring 框架如何创建 Java 对象以及客户机应用程序如何与之交互有了清晰的了解。了解了这些内容后,就更加容易理解并利用 Acegi 的方法安全性拦截器,下一节将具体介绍该主题。

 

回页首

配置 Acegi 的方法安全性拦截器

只要应用程序试图访问由 Acegi 安全系统保护的 bean 方法,请求将被自动传递到 Acegi 的方法安全性拦截器。方法安全性拦截器的作用就是控制对安全 Java bean 的方法的访问。拦截器使用 Acegi 的身份验证和授权框架确认用户是否具有权利调用安全 Java bean 的方法,然后相应地作出响应。

清单 5 展示 Acegi 的方法安全性拦截器的示例配置:

清单 5. Acegi 的方法安全性拦截器的示例配置
<bean id="privateCatalogSecurityInterceptor"
class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager">
<ref bean="authenticationManager"/>
</property>
<property name="accessDecisionManager">
<ref bean="accessDecisionManager"/>
</property>
<property name="objectDefinitionSource">
<value>
sample.PrivateCatalog.getData=ROLE_HEAD_OF_ENGINEERING
<!-- Roles required by other beans -->
</value>
</property>
</bean>

清单 5 所示的拦截器配置包含三个需要进行配置的属性,可以保护对 Java bean 的访问:authenticationManageraccessDecisionManager和 objectDefinitionSource

回忆一下,您在本系列第 1 部分的 配置身份验证处理过滤器 中曾对 authenticationManager 属性进行了配置。authenticationManager 属性的作用是对用户进行身份验证。

您在本系列的第二篇文章中了解了 accessDecisionManager 属性。这个访问决策管理器负责制定授权决策。在允许对一个安全 bean 进行访问之前,方法安全拦截器使用 authenticationManager 和 accessDecisionManager 属性对用户进行身份验证和授权。

现在查看 清单 5 中配置的 objectDefinitionSource 属性。它类似于第 1 部分中出现的 objectDefinitionSource 属性。以前的objectDefinitionSource 包含类似于 /protected/* 和 /** 这样的 URL,清单 5 中的 objectDefinitionSource 属性指定类和方法名;例如,sample.PrivateCatalog 是之前执行过代理的类的名称,而 getData 是您希望对其控制用户访问的方法的名字。

当用户访问 PrivateCatalog bean 的 getData() 方法时,控制权将自动传递给拦截器。拦截器使用 Acegi 框架检查用户的业务角色是否为ROLE_HEAD_OF_ENGINEERING(特定于本文的示例)。如果是的话,拦截器将允许对 getData() 方法进行访问。如果拦截器发现用户角色不是ROLE_HEAD_OF_ENGINEERING,则拒绝访问。

下一节将查看一个示例 Acegi 应用程序,它将实现您目前所了解的所有概念。

 

回页首

示例 Acegi 应用程序

本文的 下载源代码 包含了一个名为 AcegiMethodSecurity 的示例应用程序,可按照以下方法进行配置和部署:

  1. 使用用户信息填充 LDAP 服务器。下载的示例应用程序 包含一个 LDIF 文件,其中含有预备装载到 LDAP 服务器的用户信息。关于如何将 LDIF 文件导入到 LDAP 服务器,请参考第 2 部分的 “填充服务器” 一节。注意,该应用程序涉及与第 2 部分相同的用户(alicebob 和specialUser)。
  2. 将本文下载源代码中的 acegiMethodSecurity.war 文件复制到 Tomcat 安装目录中的 webapps 目录。
  3. 将 Acegi 的 jar 文件复制到示例应用程序的 WEB-INF/lib 文件夹。(有关内容请参考第 1 部分的 “部署和运行应用程序” 一节。 )
  4. 下载 cglib-full-2.0.2.jar 文件并将其复制到示例应用程序的 WEB-INF/lib 文件夹。

启动 Tomcat 并尝试运行示例应用程序。

运行示例应用程序

通过从浏览器访问 http://localhost:8080/acegiMethodSecurity URL 可调用示例应用程序。AcegiMethodSecurity 显示的索引页面包含两个链接(Catalog 和 Login),如图 2 所示:

图 2. 示例应用程序的主页面

使用 Acegi 保护 Java 应用程序-LMLPHP

当单击应用程序的 Catalog 链接时,它将要求您进行登录。如果以 alice 或 specialUser 的身份进行登录,示例应用程序将提供完整的 类别,包括公有数据和私有数据。这是因为在 清单 5 中,您对方法安全性拦截器进行了配置,允许用户使用 ROLE_HEAD_OF_ENGINEERING 访问私有类别,而 alice 和 specialUser 都具有该访问权。另一方面,如果您以 bob 的身份登录,示例应用程序将仅显示公有数据。

 

回页首

为通过身份验证的用户分配额外角色

本节将演示经过增强的示例应用程序。增强后的示例应用程序将展示 Acegi 如何使您能够在运行时向通过身份验证的用户临时分配额外角色。

当安全 bean(例如 清单 3 的 privateCatalog bean)要访问一个原创资源时,您可能需要使用额外的角色。例如,您可能考虑到您的安全 bean 需要通过 Java 的 Remote Method Invocation (RMI) 框架或一个 Web 服务访问某个远程应用程序。访问安全 bean 的用户不会占用远程应用程序要求访问用户所具备的业务角色。

在本例中,Acegi 首先检查用户是否经过授权来访问安全 bean。之后,Acegi 允许用户访问安全 bean。当安全 bean 试图访问远程服务时,它需要使用额外的业务角色。如果访问安全 bean 的用户不具备额外角色,安全 bean 就不能成功访问远程服务。

run-as-replacement 机制

Acegi 框架提供了一种名为 run-as-replacement 的简单机制,允许您仅在方法调用期间为通过身份验证的用户配置一个或多个额外角色。您可以使用 run-as-replacement 机制为访问远程应用程序的安全 bean 配置额外角色。这意味着只要安全 bean 需要访问远程应用程序,Acegi 将为用户装载额外角色,从而允许安全 bean 访问远程应用程序。

清单 6 对 清单 5 中的方法安全性拦截器的配置进行了增强。增强后的配置使用了 run-as-replacement 机制。

清单 6. Acegi 方法安全性拦截器的增强配置

点击查看代码清单

清单 6 使用粗体显示了两处增强(与 清单 5 相比)。第一处增强为 runAsManager 属性。runAsManager 属性的作用是向通过身份验证的用户动态添加角色。出于这个目的,runAsManager 属性包含了 RunAsManagerImpl bean 的定义。RunAsManagerImpl bean 只有在满足下面的条件时才可变为活跃状态:在 objectDefinitionSource 方法的角色定义中找到以 RUN_AS_ 为前缀的角色。例如,PrivateCatalog.getData() 方法的角色定义(清单 6 中以粗体显示的第二处增强)具有一个 RUN_AS_MANAGER 角色。

RunAsManagerImpl bean 包含一个名为 key 的属性,它封装的加密键用于确保只将额外的角色作为 run-as-replacement 程序的一部分生成。

当用户调用 getData() 方法时,RunAsManagerImpl bean 变为活跃状态并创建名为 RUN_AS_MANAGER 的额外角色,从而启用 getData() 方法访问远程应用程序。

增强的方法安全性

本文的 下载源代码 包含一个名为 EnhancedAcegiMethodSecurity 的示例应用程序,它可以演示 run-as-replacement 机制和程序。该应用程序将显示一个具有 Catalog 链接的索引页面。如果单击 Catalog 链接,将要求进行登录。

登录后,EnhancedAcegiMethodSecurity 应用程序将为您提供登录用户及其角色的完整信息。例如,如果以 alice 或 specialUser 身份登录,将向您显示用户的所有业务角色,包括额外创建的临时的 RUN_AS_MANAGER 角色。

结束语

在这份共分三部分的系列文章中,我介绍了如何使用 Acegi 安全系统增强基于 URL 的安全性和基于方法的安全性。您了解了如何设计访问控制策略并将它们托管在目录服务器中,如何对 Acegi 进行配置以与目录服务器进行通信,以及如何根据托管在服务器的访问控制策略制定身份验证和授权决策。

本系列的最后一篇文章主要介绍使用基于方法的安全性保护 Java 类实例。文章还解释了 Acegi 和 Spring 如何在内部创建和代理 Java 对象以及 bean 代理如何实现访问控制。文章包含了两个示例应用程序,您可以使用它们进一步研究本系列中学到的概念,更多有关使用 Acegi 保护 Java 应用程序的内容,请参见 参考资料 小节。

原文来自:

http://www.ibm.com/developerworks/cn/java/j-acegi1/index.html

http://www.ibm.com/developerworks/cn/java/j-acegi2/

http://www.ibm.com/developerworks/cn/java/j-acegi3/

04-15 11:58