Acegi 是一个基于 Spring 开发的安全框架,为应用程序提供基于 统一资源定位符 或 URL 和 方法访问 的细粒度安全控制和保护功能。 Acegi Security System 使用安全过滤器来提供企业应用程序的身份验证和授权服务。 Acegi 的设计仍是一个基于角色的权限控制系统,它通过一系列可配置的组件构建了一个基于 Spring IOC 组件装配模式的安全框架。在 Acegi 安全框架中也有 Principal ( 通常是用户名 ) 和 Credentials ( 通常是口令 ) 的概念,不过在 Acegi 框架中,通常将 UserDetails 作为 Principal ,除存储用户名外,还包含用户角色等权限信息。 Authentication 对象还能存储一些与认证请求有关的附加信息, e.g: 用户 IP 地址。
Acegi 使用 SecurityContext 来存储 Authentication ,默认情况下, Acegi 使用 ThreadLocal 来实现 SecurityContext 。在应用程序中通过 SecurityContextHolder.getContext().getAuthentication() 来获取 Authentication 对象 。
四大组件
Acegi Security System 由四种主要类型的组件组成:过滤器、管理器、提供者和处理程序。
1. 过滤器
这种最高级的组件提供了常见的安全服务,如身份验证、会话处理以及注销。
2. 管理器
过滤器仅是安全相关功能的高级抽象:实际上要使用管理器和提供者实现身份验证处理和注销服务。管理器管理由不同提供者提供的较低级的安全服务。
3. 提供者
有大量的提供者可用于和不同类型的数据存储服务通信,例如目录服务、关系数据库或简单的内存中的对象。这意味着您可以将用户库和访问控制协议存储在任何一种这样的数据存储服务中,并且 Acegi 的管理器将在运行时选择合适的提供者。
4. 处理程序
有时任务可能会被分解为多个步骤,其中每个步骤由一个特定的处理程序执行。比方说, Acegi 的 注销过滤器 使用两个处理程序来退出一个 HTTP 客户机。其中一个处理程序使用户的 HTTP 会话无效,而另一个处理程序则删除用户的 cookie 。当根据应用程序需求配置 Acegi 时,多个处理程序能够提供很好的灵活性。您可以使用自己选择的处理程序来执行保护应用程序所需的步骤。
安全过滤器
正如我前面提到的一样, Acegi 使用安全过滤器为企业应用程序提供身份验证和授权服务。您可以根据应用程序的需要使用和配置不同类型的过滤器。这一节将介绍五种最重要的 Acegi 安全过滤器。
Session Integration Filter
Acegi 的 Session Integration Filter ( SIF ) 通常是您将要配置的第一个过滤器。 SIF 创建了一个 安全上下文对象 ,这是一个与安全相关的信息的占位符。其他 Acegi 过滤器将安全信息保存在安全上下文中,也会使用安全上下文中可用的安全信息。
SIF 创建安全上下文并调用过滤器链中的其他过滤器。然后其他过滤器检索安全上下文并对其进行更改。比如, Authentication Processing Filter 将用户信息(如用户名、密码和电子邮件地址)存储在安全上下文中。
当所有的处理程序完成处理后, SIF 检查安全上下文是否更新。如果任何一个过滤器对安全上下文做出了更改, SIF 将把更改保存到服务器端的会话对象中。如果安全上下文中没有发现任何更改,那么 SIF 将删除它。
在 XML 配置文件中对 SIF 进行了配置,如清单 2 所示:
<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
所示:
<!---->
<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 所示:
<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 所示:
<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 和 accessDeniedHandler 。 authenticationEntryPoint 属性指定登录页面,而 accessDeniedHandler 指定 Access Denied 页面。
拦截过滤器
Acegi 的拦截过滤器 用于做出授权决策。您需要在 APF 成功执行身份验证后对拦截过滤器进行配置,以使其发挥作用。拦截器使用应用程序的访问控制策略来做出授权决定。
配置简单的访问控制策略可分为两个步骤:
<!----> 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 所示,配置所需的三个组件是 authenticationManager 、 accessDecisionManager 、 objectDefinitionSource :
- <!----> <!----> 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. 托管 Acegi 过滤器链以安全地为浏览器客户机服务的容器
下面的步骤描述了过滤器链的生命周期:
<!----> 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 如何创建一个安全上下文
现在详细地考虑下面这些步骤:
<!----> 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 如何对用户进行身份验证
现在仔细考虑以下这些步骤:
<!----> 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 应用程序
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> <property name="filterInvocationDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT /**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor </value> </property> </bean> <!-- SIF创建了一个安全上下文对象并调用过滤器链中的其他过滤器 --> <bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter" /> <!-- 管理注销处理 --> <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> <!-- Acegi使用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="/protected/protected1.jsp" /> <!-- 指定了客户在登录窗口中按下 Sign In 按钮时要访问的 URL --> <property name="filterProcessesUrl" value="/j_acegi_security_check" /> </bean> <!-- 处理身份验证和授权过程中的异常情况 --> <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> <!-- 在授权失败的情况下,呈现的Access Denied 页面 --> <property name="accessDeniedHandler"> <bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl"> <property name="errorPage" value="/accessDenied.jsp" /> </bean> </property> </bean> <!-- 用于做出授权决策的拦截过滤器 --> <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> </bean> <bean id="securityContextHolderAwareRequestFilter" class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter" /> <bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter"> <property name="key" value="changeThis" /> <property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS" /> </bean> <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager"> <property name="providers"> <list> <ref local="daoAuthenticationProvider" /> </list> </property> </bean> <bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased"> <property name="allowIfAllAbstainDecisions" value="false" /> <property name="decisionVoters"> <list> <bean class="org.acegisecurity.vote.RoleVoter" /> <bean class="org.acegisecurity.vote.AuthenticatedVoter" /> </list> </property> </bean> <bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="userDetailsService" /> <!-- UserCache property will activate the cache, it is not mandatory but increases performance by cacheing the user details retrieved from user-base --> <property name="userCache" ref="userCache" /> </bean> <bean id="userDetailsService" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl"> <property name="userProperties"> <bean class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="location" value="/WEB-INF/users.properties" /> </bean> </property> </bean> <bean id="userCache" class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache"> <property name="cache"> <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheManager"> <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" /> </property> <property name="cacheName" value="userCache" /> </bean> </property> </bean> <!-- This bean is optional; it isn't used by any other bean as it only listens and logs --> <bean id="loggerListener" class="org.acegisecurity.event.authentication.LoggerListener" /> </beans>