acegi安全验证

Acegi是专门为 Spring Web应用提供安全保护的开源框架,它通过 配置的方式就可以对业已存在的应用实施安全控制。在Acegi实施安全控制之前,必须获取操作者的身份,并进一步获知用户的权限,这样Acegi才可能对应用资源实施安全控制。
 
在本文中,我们将介绍Acegi如何对操作者进行 身份认证的内容。

将Acegi集成到Web应用程序中

Acegi通过多个不同用途的Servlet过滤器截取HTTP请求,实施访问安全的控制。按照传统的方式,我们应该在web.xml配置文件中通过<filter>定义Servlet过滤器并使用<filter-mapping>元素定义过滤器对应的URL匹配模式。

由于这些过滤器需要Spring容器中其它Bean的支持完成访问 权限控制,最原始的方式是在过滤器中通过硬编码的方式访问Spring容器中的Bean:

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
FooBean foo = (FooBean)ctx.getBean(“foo”);

这种硬编码的方式显然不太方便,如何让过滤器既能够正确工作又能享受到Spring IoC的好处呢?Acegi通过“过滤器代理”来解决这个问题。

代理Acegi的过滤器

org.acegisecurity.util.FilterToBeanProxy使用Spring容器中的Bean代理过滤器,FilterToBeanProxy是一个标准的Servlet过滤器。在内部FilterToBeanProxy通过WebApplicationContext访问Spring容器,并将过滤器处理逻辑委托给一个Spring容器中的Bean。这样,我们就可以在web.xml中定义FilterToBeanProxy,声明过滤器匹配的URL模式,并在Spring容器中充分利用IoC进行定义委托的Bean。

FilterToBeanProxy支持两个初始化参数:

l targetClass:委托Bean的全限定类名,委托类必须实现过滤器接口。你必须在Spring容器中至少定义一个该类的Bean,否则将抛出异常。当Spring容器中拥有多个该委托类的Bean时,FilterToBeanProxy选择第一个委托类的Bean(这种情况应该避免);

l targetBean:当然你也可以通过一个Bean名指定委托的Bean。
图 1说明了Acegi是如何通过FilterToBeanProxy将Servlet容器和Spring容器结合起来共同定义一个功能齐备的Servlet过滤器的:

使用Acegi进行身份认证(之一)(图一)
图 1 通过过滤器代理将Servlet容器和Spring容器结合起来



org.acegisecurity.securechannel.ChannelProcessingFilter是一个Acegi的过滤器,它负责处理安全通道的转换。如果直接在web.xml中定义ChannelProcessingFilter,我们将很难在不编写代码的情况下,使其引用Spring容器中的Bean。借助FilterToBeanProxy的帮助,我们就可以放心地在Spring容器中配置ChannelProcessingFilter,得到一个完整可用实例。当然ChannelProcessingFilter是需要服务于特定URL请求的,而过滤器URL映射只能在Servlet容器的web.xml中定义,这个工作由FilterToBeanProxy对应的<filter-mapping>元素完成。

如果我们将Servlet容器和Spring容器比作牛郎和织女,那么FilterToBeanProxy无疑就是鹊桥了。事实上,Acegi本身并不依赖于FilterToBeanProxy,它是一种充分利用Spring容器依赖注入好处配置Servlet过滤器的一个方法,正因为如此,所以Acegi社区中有很多开发者建议FilterToBeanProxy应该从Acegi中剥离出来,将其加入到Spring的核心类库中。

 

 

使用代理过滤器链对处理HTTP请求

Acegi通过众多的过滤器完成不同安全控制的任务,在前面我们提到了三个过滤器:HttpSessionContextIntegrationFilter、ExceptionTranslationFilter和ChannelProcessingFilter。除此以外,还有LogoutFilter、AuthenticationProcessingFilter等不下10个的过滤器。

当需要配置多个Servlet过滤器时,虽然我们可以通过FilterToBeanProxy分别进行配置,但这将导致冗长难看的web.xml,同时还需要小心谨慎地通过过滤器的配置顺序保证它们的调用顺序。为了解决这个问题,Acegi在0.8版本中添加了一个org.acegisecurity.util.FilterChainProxy。FilterChainProxy可以同时指定多个过滤器并将它们组成一个过滤器链,而FilterToBeanProxy只要将代理目标设置为FilterChainProxy就可以了。来看下面的配置:

代码清单 1 web.xml

 
 
<web-app> <context-param> ①指定 Spring配置文件 <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext.xml, classpath:applicationContext-acegi-plugin.xml </param-value> </context-param> <filter>②将过滤器委托给FilterChainProxy <filter-name>AcegiFilterChainProxy</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>③对所有的URL进行过滤 <filter-name>AcegiFilterChainProxy</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> … </web-app>

在web.xml中我们首先需要定义Spring的配置文件,以便将Spring容器集成到Servlet容器中。紧接着,我们定义一个FilterToBeanProxy的过滤器拦截所有URL请求并委托给FilterChainProxy进行处理。FilterChainProxy在Spring容器中定义如下所示:

代码清单 2 applicationContext-acegi-plugin.xml

 
 
<beans> <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">①目标委托的Bean <property name="filterInvocationDefinitionSource"> <value> CONVERT_URL_TO_UPPERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT ②多个过滤器组成的过滤器链 /**=channelProcessingFilter,httpSessionContextIntegrationFilter,logoutFilter </value> </property> </bean> <bean id="channelProcessingFilter" ③过滤器链中的过滤器(1) class="org.acegisecurity.securechannel.ChannelProcessingFilter"> … </bean> <bean id="httpSessionContextIntegrationFilter" ④过滤器链中的过滤器(2) class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"> … </bean> … </beans>

在①处定义的FilterChainProxy Bean是web.xml的FilterToBeanProxy的委托目标。FilterChainProxy通过filterInvocationDefinitionSource定义多个相互链接的过滤器,如②所示。这些过滤器以逗号分隔,它表示web.xml中定义的匹配URL(/*)请求将分别通过channelProcessingFilter、httpSessionContextIntegrationFilter及logoutFilter这三个过滤器的拦截处理。这个过滤器链通过过滤器对应的Bean名字(如③和④)进行定义,过滤器都实现了javax.servlet .Filter接口。你完全可以参照实例方式定义多组滤器链,以便处理不同URL请求。不过,在一般情况下,你只要配置一个过滤器链就可以了。

filterInvocationDefinitionSource属性由两类信息组成:其一是指令信息,如

CONVERT_URL_TO_UPPERCASE_BEFORE_COMPARISON表示判断URL匹配时,首先将URL转变为大写的格式,如果设置为CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON则表示比较前事先转换为小写的形式。而PATTERN_TYPE_APACHE_ANT表示使用Ant路径风格进行匹配URL的描述,如果不提供这个指令,Acegi使用正则表达式来解析URL路径映射。

 

身份认证管理

使用Acegi保护应用程序的第一步是根据用户提供的认证信息进行身份认证,以确定用户的身份,获取对应的权限信息,准备好Authentication。通过认证的Authentication拥有权限信息,它是Acegi进行后续安全对象访问安全控制的依据。

 

Acegi对所有类型安全对象的访问控制机制基本相同:判断Authentication是否已经包含访问安全对象所需的权限。但Acegi进行身份认证的方式则多姿多彩、五花八门,身份认证方式的多样性源于认证系统的多样性。由于篇幅所限,不可能面面俱到的逐一讲解所有的认证方式,我们将以最常见的基于数据库的认证方式并对其它的认证方式进行简单的概述。

基于内存存储用户信息的身份认证

如果你开发的系统仅有少数几个固定的用户,且用户信息管理的功能很简单,这时你可以将用户直接保存在配置文件中,在系统启动将使用户信息常驻于内存中。下面,我们就从这个最简单的身份认证方式出发开始学习Acegi实际应用的征程。
代码清单 3 applicationContext-acegi-plugin.xml
身份认证过滤器

 
 
<beans> <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> <property name="filterInvocationDefinitionSource"> <value> … /**=authenticationProcessingFilter ①使用认证处理过滤器处理匹配的URL </value> </property> </bean> <bean id="authenticationProcessingFilter" ②认证处理过滤器 class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> ②-1过滤器处理的URL <property name="filterProcessesUrl" value="/j_acegi_security_check"/> <property name="defaultTargetUrl" value="/main.jsp"/> ②-2认证成功后转向的URL ②-3认证失败后转向的URL <property name="authenticationFailureUrl" value="/index.jsp?login_error=1"/> </bean> </beans>

首先,在①处通过FilterChainProxy定义了仅包含一个authenticationProcessingFilter的过滤器链。这个过滤器链匹配于所有URL的请求,换句话说,authenticationProcessingFilter有机会对所有URL请求进行拦截处理,而在这所有这些URL请求中,究竟哪个URL请求才是用户身份认证的请求呢?如果你心中也拥有这个疑问,则说明你正走在正确的理解问题的思路上。

authenticationProcessingFilter在②-1、2、3处,定义了一些URL,它们分别对应身份认证请求的URL、认证成功后转向的URL以及认证失败后转向的URL。其中②-1处的filterProcessesUrl属性表示这个URL是用户提交身份信息并要求进行身份认证的HTTP请求(一般是一个包含用户名/密码的表单HTTP POST请求),“/j_acegi_security_check”是默认的设置,之所以在此显式给出设置值,是为了说明你可以根据实际需要进行调整。

现在,authenticationProcessingFilter已经知道了申请进行用户身份认证URL,但它如何知道这个HTTP请求分别通过什么参数代表用户名和密码呢?Acegi框架规定必须使用j_username和j_password作为用户名和密码的HTTP参数名。这两个参数名是固定的,Acegi不允许对此进行配置,不过这种约定俗成而非事事配置的风格正渐渐成为流行的设计模式。

我们来看一下提供用户名/密码输入表单的登录页面:
代码清单 4 index.jsp:登录页面

 
 
<%@ page contentType="text/html;charset=UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <html> <body> <c:if test="${not empty param.login_error}"> ① <font color="red">用户名或密码错误。</font> </c:if> ②处理用户身份认证服务所对应的URL <form name="form1" method="post" action="<c:url value="/j_acegi_security_check"/>"> 用户名:<input type="text" name="j_username"/><br/>③用户名的参数名 密 码:<input type="password" name="j_password"/><br/>④用户密码的参数名 <input type="submit" value="登录"/> </form> </body> </html>

 ②处对应代码清单 3中②-1处的设置值,它表示处理用户身份认证服务所在的URL,而③和④是Acegi规定的承载用户名、密码的参数名。当这个表单提交后authenticationProcessingFilter将从请求中抽取j_username和j_password参数的值,两者组成了代表请求认证的用户信息,Acegi使用这个信息构造Authenticaion实例,并封装成SecurityContext放入到SecurityContextHolder中,然后启动用户身份认证的流程。当认证成功后,页面转向代码清单 3中②-2处所定义的/main.jsp,如果认证失败则转向代码清单 3中②-3处所定义的/index.jsp?login_error=1。由于登录失败后也转向index.jsp页面,所以代码清单 4的①处专门对此进行了处理,当发现请求参数包含login_error时,打出一行提示登录失败的信息。

现在,我们已经了解了authenticationProcessingFilter从何处获取需要进行认证的用户信息,并对认证成功和失败后的转向进行了定义。那么authenticationProcessingFilter究竟如何对用户信息(用户名/密码)的合法性进行认证呢?

authenticationProcessingFilter其实只负责获取需要认证的用户信息并根据结果完成页面转向,它相当于一个客户受理的窗口,真正的用户身份认证工作则交由后台的AuthenticationManager完成。

 

下面

我们为authenticationProcessingFilter注入AuthenticationManager,后者将完成具体的身份认证工作:
代码清单 5 applicationContext-acegi-plugin.xml
认证管理器的配置

 
 
 
<beans> … <bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> <property name="filterProcessesUrl" value="/j_acegi_security_check"/> <property name="defaultTargetUrl" value="/main.jsp"/> <property name="authenticationFailureUrl" value="/index.jsp?login_error=1"/> ①注入认证管理器 <property name="authenticationManager" ref="authenticationManager" /> </bean> ②认证管理器 <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager"> <property name="providers"> <list> ②-1使用基于DAO的认证提供者提供认证服务 <ref local="daoAuthenticationProvider" /> </list> </property> </bean> <bean id="daoAuthenticationProvider" ③基于DAO的认证提供者 class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"> ③-1根据用户名获取系统中真实UserDetails 对象的服务类 <property name="userDetailsService" ref="userDetailsService" /> </bean> ④ 该服务类根据缓存在内存中的用户信息列表获取userDetails对象 <bean id="userDetailsService" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl"> <property name="userMap"> ④-1用户信息 <value> john=john,PRIV_COMMON,PRIV_1 tom=tom,PRIV_COMMON,PRIV_1,PRIV_2 peter=peter,disabled,PRIV_COMMON,PRIV_1 </value> </property> </bean> </beans>

在①处,我们为authenticationProcessingFilter注入了一个AuthenticationManager Bean。AuthenticationManager有两个实现类:
l MockAuthenticationManager:这个实现类用于开发时的测试环境,它将所有待认证的用户标志为 有效 有户;
l ProviderManager:该实现类将用户身份认证的工作委托给多个提供者来完成。

这里,我们使用ProviderManager来定义一个AuthenticationManager Bean,如②所示。可以通过ProviderManager的providers属性配置多个认证提供者,认证提供者必须实现org.acegisecurity.providers.AuthenticationProvider接口。身份认证的多样性通过AuthenticationProvider接口实现类的多样性体现出来,Acegi针对不同的安全系统提供了10多个不同的认证提供者。这些不同的认证提供者在表 1中进行说明: 

使用Acegi进行身份认证(之一)(图二)


这里,我们使用DaoAuthenticationProvider,在②-1所示。它负责从数据库或其它保存用户信息的媒介中获取用户信息进行认证。

DaoAuthenticationProvider首先从SecurityContextHolder的Authentication中得到待认证的用户名,并根据该用户名获取保存在数据库或其它媒介中代表真正系统用户的UserDetails对象。紧接着比较Authentication和UserDetails的匹配关系(如看密码是否相等),如果两者匹配,认证成功,并将UserDetails的权限信息将拷贝到Authentication中。如果不匹配,认证将失败。

DaoAuthenticationProvider通过UserDetailsService完成UserDetails的获取工作,根据存储用户信息媒介的不同,Acegi提供了两个UserDetailsService的实现类:

l InMemoryDaoImpl:该实现类负责从内存中获取用户的信息,它允许通过UserMap或Properties直接在Spring配置文件中定义系统用户信息。

l JdbcDaoImpl:该实现类从数据库中获取用户的信息。我们将在下一节讨论该实现类的使用方法。

在这里,我们使用InMemoryDaoImpl获取UserDetails,如③-1所示并通过userMap属性定义系统用户信息,如④-1所示。userMap属性的类型为UserMap,Acegi已经向Spring注册了对应的编辑器,所以我们可以通过格式化的字符串为UserMap类型的属性提供配置值。在④-1处,我们提供了几行键值对的配置值,每一行代表一个用户及其对应的权限信息。下面我们通过图 2来了解代表一个用户的字符串的格式:

使用Acegi进行身份认证(之一)(图三)


8 用户配置信息

我们通过④-1定义了三个用户,john、tom和peter,它们对应的密码和用户名相同。其中john和tom是激活的用户,而peter用户处于非激活状态。john和peter都拥有PRIV_COMMON,PRIV_1权限,而tom额外拥有PRIV_2的权限。

如果用户数比较多,在Spring中直接进行配置未免不太雅观,这时,你可以将用户信息转移到一个属性文件中,并通过userProperties进行加载:

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

users.properties文件内容的格式和刚才我们介绍的方式相同,既每一个用户对应一行格式化配置串。

通过以上分析后,我们可以理出参与用户身份认证所涉及到4个Bean,它们之间的关系可通过图 9来描述:

使用Acegi进行身份认证(之一)(图四)

图 9 基于内存用户信息认证的主要Bean之间的关系

我们知道,大部分应用系统的用户信息并非一个小小的属性文件可以应付的,它们一般保存在数据库中。InMemoryDaoImpl存在的更多意义在于方便程序测试——你可以不依赖外部数据库环境完成测试。在生产环境下,我们应该使用JdbcDaoImpl这个UserDetailsService实现类从数据库中获取UserDetails。

 

这种替换的难度很小,仅需要配置一个JdbcDaoImpl Bean并将其作为daoAuthenticationProvider的userDetailsService的实现即可。

 

JdbcDaoImpl其实是基于Spring JDBC技术进行数据库访问的,它继承于org.springframework.jdbc.core.support.JdbcDaoSupport。所以JdbcDaoImpl必不可少的一个属性是dataSource,它指定一个保存用户信息的数据源。

JdbcDaoImpl通过usersByUsernameQuery和authoritiesByUsernameQuery属性定义查询用户信息和用户权限的SQL语句。实际上,JdbcDaoImpl为以上两个属性提供了默认的SQL语句,分别是:

"SELECT username,password,enabled FROM users WHERE username = ?"



"SELECT username,authority FROM authorities WHERE username = ?"

一般情况下,我们会使用自己的用户和权限表,所以你需要自定义以上两个属性。JdbcDaoImpl将Authentication中待认证用户名作为这两个SQL语句的username参数值,并执行查询返回结果,然后根据列索引的方式从结果集中取得数据构造UserDetails对象,所以在构造SQL语句时必须保持用户信息字段位于正确的列位置,字段名可以任意指定。

应该说,JdbcDaoImpl还不是非常实用的UserDetailsService实现类,因为用户对象(UserDetails)除包含用户名/密码、是否激活、权限等信息外,还经常需要包含一些诸如email、telephone等业务相关的信息。而JdbcDaoImpl通用类并不知道我们具体的业务需求,所以我们往往需要通过实现UserDetailsService接口提供自己的实现类来完成这些工作。编写一个自己的UserDetailsService实现类不费什么力气,你只要实现接口中的UserDetails loadUserByUsername(String username)方法根据用户名获取UserDetails对象即可。在编写好自己的UserDetailsService后,在Spring配置并替换①处的userDetailsService属性即可,限于篇幅,我们不再展开论述,读者可以试着自行完成。

小结
Acegi通过过滤器对请求实施拦截,当发现用户没有登录时,将强制用户进行系统登录,通过一定的接口规范,Acegi就可以获取操作者的用户身份,并进而获取用户的权限。在下一篇文章中,我们将接着介绍身份认证的高级话题。

 

高级部分

在《使用Acegi保护您的应用(之一)》的文章中,我们讲解了如何将Acegi集成到基于Spring的Web应用中,并介绍了基于Acegi身份认证管理的基本处理。在这篇文章里,我们将继续Acegi身份认证的高级部分。此外,我们还将讲解实施高级身份认证的具体做法。

密码加密的问题

不管是使用InMemoryDaoImpl还是JdbcDaoImpl,它们的工作是根据待认证用户名获取UserDetails对象。在获取UserDetails后,DaoAuthenticationProvider要做的工作是比较Authentication和UserDetails的匹配关系给出认证成功还是失败的认证结果。

但是DaoAuthenticationProvider根据什么规则判断Authentication和UserDetails的匹配关系呢?简单来说,就是判断以下两者的关系:
Authentication#getCredentials() 是否匹配于UserDetails#getPassword()

如果数据库或内存中保存的用户密码是明文的,那么这一比较过程再简单不过了。但是如果用户密码是加密的,也即UserDetails#getPassword()是加密的密码,由于用户提供的密码(Authentication#getCredentials())是用户的直接输入值,它是非加密的,这时如何判断Authentication#getCredentials()的正确性呢?

这就引出了本节所要介绍的两个关键接口:org.acegisecurity.providers.encoding. PasswordEncoder和org.acegisecurity.providers.dao.SaltSource。PasswordEncoder完成两件工作:

1) 对明文的密码(Authentication#getCredentials())进行编码,得到对应的加密值;

2) 对处于非对称状态(一个是加密的,另一个是明文的)两个密码进行比较并给出判断结果。

PasswordEncoder进行密码比较时,需要使用到一个SaltSource,它代表一个“加密盐”(是专业术语,但称之为加密种子更容易理解一些),对用户提供的密码进行加密时采用的加密盐必须和系统中保存的的用户加密密码所采用加密盐相同。

PasswordEncoder定义了两个接口方法:

l String encodePassword(String rawPass, Object salt):对原始未加密的密码通过一定的算法进行加密运算,salt为加密时所用的加密盐。一般使用的是MD5或SHA摘要算法,因为摘要算法有两个特性:1)两个相同的字符串摘要值是相同的;2)通过摘要值不能反推出原来的字符串。这两个特性使得以摘要的方式保存密码是安全的——你无法通过密码摘要获取原始密码。而加密盐使密码安全系数进一步提高:对于相同的密码,只要加密盐不相同,计算出的摘要也是不同的。这里的rawPass是指Authentication#getCredentials().toString();

l boolean isPasswordValid(String encPass, String rawPass, Object salt):通过算法判断待认证用户所提供的密码是否是有效的。在计算过程中,需要对rawPass使用salt加密盐进行加密运算以得到加密值,再同encPass比较就可以得出判断结果。这里,encPass是指UserDetails#getPassword(),而rawPass是指Authentication#getCredentials().toString()。

Acegi在org.acegisecurity.providers.encoding包中提供了几个常见的PasswordEncoder实现类,在表 2中介绍:

表 2 密码编码器



SaltSource接口仅有一个Object getSalt(UserDetails user)方法,Acegi为其提供了两个实现类,它们分别是:

l org.acegisecurity.providers.dao.salt.ReflectionSaltSource:它允许你在UserDetails中提供一个代表加密盐的属性,属性名通过userPropertyToUse指定。ReflectionSaltSource通过反射机制获取UserDetails中的加密盐,所以该实现类允许不同的用户采用不同的加密盐;

l org.acegisecurity.providers.dao.salt.SystemWideSaltSource:该实现类不允许不同用户采用各自的加密盐,它采用全局范围统一的加密盐。你可以通过systemWideSalt指定加密盐的值。

假设,我们保存在T_USER表中的password字段采用了MD5加密并且统一采用全局唯一的加密盐。这时,我们就有必要调整代码清单 7中daoAuthenticationProvider的配置以应用密码编码器:

代码清单 8 applicationContext-acegi-plugin.xml

使用密码编码器

  
  
<bean id="daoAuthenticationProvider " class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="userDetailsService" /> <property name="passwordEncoder"> ①采用MD5加密的密码编码器 <bean class="org.acegisecurity.providers.encoding.Md5PasswordEncoder" /> </property> <property name="saltSource"> ②采用全局统一的加密盐 <bean class="org.acegisecurity.providers.dao.salt.SystemWideSaltSource"> <property name="systemWideSalt" value="baobaotao"/> </bean> </property> </bean>

 

 

成功登录系统的后置处理

一般的业务系统在用户登录成功后,需要在数据库中记录一条相应的用户登录日志。我们可以通过Acegi提供的事件机制来完成这一功能。当用户身份认证成功后,Acegi会产生一个AuthenticationSuccessEvent事件,该事件是org.springframework.context.ApplicationEvent的子类,所以AuthenticationSuccessEvent是一个Spring容器事件。我们可以定义一个监听器响应该事件以记录用户登录日志,请看LoginSuccessListener的代码:

代码清单 9 LoginSuccessListener

   
   
package com.baobaotao.service; import org.acegisecurity.Authentication; import org.acegisecurity. event .authentication.AuthenticationSuccessEvent; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; public class LoginSuccessListener implements ApplicationListener { public void onApplicationEvent(ApplicationEvent event ) { if ( event instanceof AuthenticationSuccessEvent) {①判断是否是认证成功的事件 AuthenticationSuccessEvent authEvent = (AuthenticationSuccessEvent) event ; Authentication auth = authEvent.getAuthentication(); String userName = auth.getName(); ②这里,我们仅通过一条输出信息的语句模拟记录登录日志的操作 System. out .println( " 模拟记录用户[ " + userName + " ]成功登录日志... " ); } } }

通过扩展Spring的ApplicationListener接口定义一个监听Acegi AuthenticationSuccessEvent事务的监听器。在实际系统中,你可以使用DAO替换②处的逻辑,执行记录用户登录日志的操作。在编写好LoginSuccessListener后,剩下的工作就是在Spring容器中声明这个监听器,仅需要一行简单的配置就可以了:

   
   
< bean class = " com.baobaotao.service.LoginSuccessListener " />

这样,当用户登录并通过了Acegi的用户身份认证后, LoginSuccessListener监听器的onApplicationEvent()方法将接收到AuthenticationSuccessEvent事件。

在多个请求之间共享SecurityContext
我们知道SecurityContext保持着Acegi框架重要的Authentication对象,而SecurityContext保存在SecurityContextHolder中。然而SecurityContextHolder只为SecurityContext对象提供请求线程范围内的生命周期。也就是说,当一个登录认证请求结束后,用户相关的SecurityContext对象将从SecurityContextHolder中清除了。当用户发起下一个请求时,又必须重新进行身价认证,如何使SecurityContext在Session级别中进行共享呢?

Acegi通过HttpSessionContextIntegrationFilter解决这个问题,当一个请求到达时,它尝试从Session中获取用户关联的SecurityContext并将其放入到SecurityContextHolder中,当请求结束时,HttpSessionContextIntegrationFilter又将SecurityContext转存到HttpSession中。这样,Acegi就通过HttpSessionContextIntegrationFilter将SecurityContext对象在请求级的SecurityContextHolder和Session级的HttpSession中摆渡,从而保证SecurityContext可以在多个请求之间共享。

在过滤器链中,HttpSessionContextIntegrationFilter必须位于认证处理过滤器之前,这样认证处理过滤器当发现SecurityContextHolder中已经拥有和用户关联的经过认证的Authentication时,就可以短路掉用户身份认证的步骤:

代码清单 10 applicationContext-acegi-plugin.xml

通过HttpSession转存请求之间的SecurityContext

   
   
< bean id = " filterChainProxy " class = " org.acegisecurity.util.FilterChainProxy " > < property name = " filterInvocationDefinitionSource " > < value > /* *=httpSessionContextIntegrationFilter,authenticationProcessingFilter ① </value> </property> </bean> <bean id="httpSessionContextIntegrationFilter" ②通过HttpSession转存SecurityContext的过滤器 class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/> …

在①处,我们将httpSessionContextIntegrationFilter放置在authenticationProcessingFilter之前。如果用户还未通过身份认证,httpSessionContextIntegrationFilter在HttpSession中找不到对应的SecurityContext,这时authenticationProcessingFilter将启用正常的认证流程。反之,如果已经通过了身份认证,SecurityContext将直接从HttpSession中获取。

 

退出系统的后置处理

SecurityContext保存在HttpSession中,当用户退出系统时必须清除之,否则SecurityContext将一直保存在HttpSession中,需要等到Session过期后才会被清除,这将造成额外的内存消耗。从另外一个方面说,在退出系统时常常需要执行一些相关的操作,如记录用户退出系统的日志、将登录信息保存到Cookie中等。

Acegi为完成以上一系列由退出系统引发的操作,专门提供了一个退出过滤器:org.acegisecurity.ui.logout.LogoutFilter,它允许我们通过配置完成相关的操作:

代码清单 11 applicationContext-acegi-plugin.xml

退出系统后置处理配置

   
   
< bean id = " filterChainProxy " class = " org.acegisecurity.util.FilterChainProxy " > < property name = " filterInvocationDefinitionSource " > < value > /* *=httpSessionContextIntegrationFilter,authenticationProcessingFilter,logoutFilter ① </value> </property> </bean> <bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">②退出系统过滤器 <constructor-arg> ②-1退出系统前需要执行的操作 <list> <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" /> </list> </constructor-arg> <constructor-arg value="/index.jsp" />②-2退出系统后转向的URL ②-3用于响应退出系统请求的URL <property name="filterProcessesUrl" value="/j_acegi_logout"/> </bean> …

在①处,我们在过滤器链中添加一个logoutFilter,它负责处理用户退出系统的操作。退出系统过滤器需要以下三方面的信息:

1) 哪一个URL是退出系统的HTTP请求,这通过filterProcessesUrl属性指定,②-3所示。LogoutFilter默认的退出系统URL即是“/j_acegi_logout”,这里显式进行配置是为了说明可以根据需要进行调整;

2) 退出系统时需要执行哪些处理器,通过LogoutFilter的构造函数指定,如②-1所示。处理器必须实现org.acegisecurity.ui.logout.LogoutHandler接口,Acegi为该接口提供了两个实现类,分别是SecurityContextLogoutHandler和 TokenBasedRememberMeServices,前者将SecurityContext从HttpSession中删除,而后者将Anthentication中的用户名/密码保存到客户端的Cookie中,以便下次用户访问系统时直接通过Cookie中的用户名/密码进行自动登录。我们将在下一节学习到TokenBasedRememberMeServices的知识。

3) 退出系统后转向哪个URL,通过构造函数参数指定,如②-2所示。

配置好退出系统过滤器后,在需要在系统页面中提供一个退出系统的操作链接:

<A href="<c:url value="/j_acegi_logout"/>">退出系统</A>

注意粗体所示代码代表退出系统所对应的URL,它必须和LogoutFilter的filterProcessesUrl属性一致。这样,当用户点击页面中的“退出系统”链接后,LogoutFilter拦截这个URL请求,并调用SecurityContextLogoutHandler将SecurityContext从HttpSession中清除掉,最后转向/index.jsp页面。

实施Remember-Me认证
很多网站的用户登录页面都提供了一个类似于“两周内不用再登录”、“记住我的帐号”等功能,其原理是在用户登录成功后使用客户端浏览器的Cookie记录用户登录信息,当下次再访问相同站点时,直接从Cookie中取得用户登录信息并进行自动登录。这即是经典的Remember-Me的功能,该功能在一定程度上降低了用户频繁登录的麻烦。根据系统安全性需求的不同,Remember-Me可能在Cookie中保存用户名/密码或仅保存用户名,前者可以完成自动登录,而后者只是让用户避免输入用户名。

如果在Cookie中记录用户名/密码,虽然可以避免每次访问网站都进行登录的麻烦,但这把双刃剑的反面是黑客可以在一定条件下获取这个免检的通行证。为了在给用户带来便利的同时尽力降低潜在的风险,Cookie保存用户名/密码的方式变得非常关键,以下几点是必须考虑的问题:

1) Cookie是易受攻击的,多用户共享浏览器和跨站点脚本攻击都可能使Cookie失窃;

2)将用户名/密码保存在Cookie中,意味着用户可以在不显式进行登录的情况下,获取正常登录的一切权限;

3)一切可以从Cookie中反推出密码明文的存储方式都是不可接受的;

4)必须将客户端IP信息绑定在Cookie中,这样即使Cookie失窃,也不可能在其它机器使用。

如果说HttpSessionContextIntegrationFilter通过HttpSession使Authentication获得跨请求共享的能力,那么Remember-Me则通过Cookie使Authentication获得跨多个Session的能力。Remember-Me功能可以视为一套解决方案,以下是Remember-Me中最关键的三个问题:

1) 在用户登录时,获取用户名/密码的信息,并将其以一定方式保存到Cookie中;

2) 在Cookie有效时间内,当用户访问站点安全页面时,自动进行登录;

3) 必须提供一个功能,让用户可以手工清除Remember-Me Cookie。

org.acegisecurity.ui.rememberme.RememberMeServices是Remember-Me方案中最关键的一个接口,它定义了以下几个方法:

l void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication):登录成功后调用该方法,将用户名/密码保存到Cookie中;

l void loginFail(HttpServletRequest request, HttpServletResponse response):登录失败后调用该方法;

l Authentication autoLogin(HttpServletRequest request, HttpServletResponse response):从Cookie中自动获取用户名/密码进行自动登录。

loginSuccess()和loginFail()方法的调用已经编制到Acegi的AbstractProcessingFilter抽象过滤器中,这意味着任何注入了RememberMeServices实例的过滤器都会以适合的方式调用这两个方法。而autoLogin()方法则通过RememberMeProcessingFilter进行调用,当RememberMeProcessingFilter发现SecurityContextHolder中不存在有效的Authentication时,autoLogin()方法就会被执行。

Acegi为RememberMeServices接口提供了两个实现类,它们分别是:

l NullRememberMeServices:类似于适配器的实现类,它不做任何有意义的事情,这是AbstractProcessingFilter中默认的实现类;

l TokenBasedRememberMeServices:基于凭证(一般指用户名/密码)的Remember-Me实现类,它真实地实现了接口中的方法。

在登录时将用户名/密码记录到Cookie中
我们第一个要做的工作是通过调整AuthenticationProcessingFilter的配置,在处理用户登录页面提交的用户认证信息时,将用户名/密码通过Response记录到客户端的Cookie中:

代码清单 12 applicationContext-acegi-plugin.xml

记录Remember-Me的Cookie信息

   
   
< bean id = " authenticationProcessingFilter " class = " org.acegisecurity.ui.webapp.AuthenticationProcessingFilter " > … ①注入一个RememberMeServices < property name = " rememberMeServices " ref = " rememberMeServices " /> </ bean > < bean id = " rememberMeServices " ②RememberMeServices配置 class = " org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices " > < property name = " tokenValiditySeconds " value = " 432000 " /> - 1Cookie有效时间,单位为秒 < property name = " key " value = " baobaotao " /> - 2 Cookie中的键值 </ bean >

通过以上的配置,我们在处理用户登录的同时将用户名/密码的信息记录到Cookie中。authenticationProcessingFilter在完成用户身份认证后,如果认证成功,调用rememberMeServices的loginSuccess()方法,该方法将用户名/密码按以下方式进行编码,编码后再写到客户端的Cookie中:

   
   
base64(username + " : " + expirationTime + " : " + md5Hex(username + " : " + expirationTime + " : " + password + " : " + key))

base64()表示进行BASE64编码操作,而md5Hex()表示进行MD5摘要并将结果值以HEX(十六进制)进行编码。注意计算式中粗体所示key操作项,Acegi通过key防止整个加密串被恶意篡改。因为key是在服务端中指定的值,黑客无法进行猜测,在服务端通过如②-2所示的key属性指定该值。

Cookie的有效时间通过tokenValiditySeconds指定,默认为两个星期。②-1处我们显式指定为5天(对应的秒数)。

我们知道authenticationProcessingFilter处理对应/j_acegi_security_check的请求,我们应该让用户决定是否使用Remember-Me的功能,这可以通过一个名为“_acegi_security_remember_me”的HTTP参数来决定,当登录请求表单包含该参数时(勾选上对应的复选框),服务端认为需要启用Remember-Me功能,否则不启用Remember-Me功能。所以,我们必须相应地调整登录页面表单,如代码清单 13所示:

代码清单 13 index.jsp:添加是否使用Remember-Me功能的控制参数

   
   
<% @ page contentType = " text/html;charset=UTF-8 " %> <% @ taglib prefix = " c " uri = " http://java.sun.com/jsp/jstl/core " %> < form name = " form1 " method = " post " action = " <c:url value= " / j_acegi_security_check " /> " > 用户名: < input type = " text " name = " j_username " />< br /> 密 码: < input type = " password " name = " j_password " />< br /> ①用户可以通过勾选或取消该复选框决定是否启用Remember - Me功能 < input type = " checkbox " name = " _acegi_security_remember_me " > 5天内不用再登录 < input type = " submit " value = " 登录 " /> </ form >
 

根据Remember-Me进行自动登录

如果用户在登录时选择了Remember-Me的功能(即勾选“5天内不用再登录”复选框),登录成功后用户名/密码的信息就保存在客户机的Cookie中。下次用户直接访问站点的安全页面时,必须有一个过滤器能够自动调用RememberMeServices#autoLogin()完成自动登录的操作,这便是通过RememberMeProcessingFilter过滤器来完成的:

代码清单 14 applicationContext-acegi-plugin.xml

    
    
Remember - Me自动登录 < bean id = " filterChainProxy " class = " org.acegisecurity.util.FilterChainProxy " > < property name = " filterInvocationDefinitionSource " > < value > /* *=httpSessionContextIntegrationFilter,authenticationProcessingFilter, logoutFilter,rememberMeProcessingFilter ①处理自动登录的过滤器 </value> </property> </bean> <bean id="rememberMeProcessingFilter" ②自动登录过滤器 class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter"> <property name="rememberMeServices" ref="rememberMeServices"/>②-1 <property name="authenticationManager" ref="authenticationManager"/>②-2 </bean> <bean id="rememberMeServices" ③ class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices"> <property name="tokenValiditySeconds" value="432000"/> <property name="key" value="baobaotao"/> <property name="userDetailsService" ref="userDetailsService" />③-1 </bean> <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager"> <property name="providers"> <list> <ref local="daoAuthenticationProvider"/> <bean class="org.acegisecurity.providers.rememberme. ④ RememberMeAuthenticationProvider"> <property name="key" value=" baobaotao"/> </bean> </list> </property> </bean>

首先,我们在过滤器链中添加一个rememberMeProcessingFilter,它负责对所有HTTP请求进行拦截,当发现SecurityContextHolder中没有包含有效的Authentication对象时,自动调用RememberMeServices#autoLogin()方法从Cookie中获取用户名/密码的编码串进行自动登录。所以rememberMeProcessingFilter首先要注入一个RememberMeServices Bean,如②-1所示。

在 代码清单 12中已经定义了一个被AuthenticationProcessingFilter 使用的RememberMeServices Bean。我们在前面说过, AuthenticationProcessingFilter在注入RememberMeServices Bean后,就会在适合的时候调用RememberMeServices#loginSuccess()方法将用户凭证保存到Cookie中。而RememberMeProcessingFilter则会调用RememberMeServices#autoLogin()方法对保存在Cookie中的用户凭证执行自动认证的操作。前者是将Authentication中的用户名/密码写入到Cookie中,而后者需要据此获得对应的UserDetails,进而再重现出Authentication对象。正如你所想到的一样,RememberMeServices需要通过一个UserDetailsService来完成这项工作,所以我们需要调整RememberMeServices的配置,如③-1所示。

rememberMeProcessingFilter通过rememberMeServices获取对应Cookie中用户的UserDetails后,就必须进行用户身份认证。这项工作依然委托给authenticationManager完成,所以在②-2中,我们给rememberMeProcessingFilter注入了authenticationManager Bean。

authenticationManager如何对基于Cookie的用户凭证进行认证呢?显然,不能采用原来的daoAuthenticationProvider所用的方法,因为Cookie所提供用户凭证和登录表单提供的用户凭证在格式上存在很大的差异。基于Remember-Me的用户名/密码信息是经过特殊编码的字符串,Acegi通过RememberMeAuthenticationProvider负责对基于Cookie的用户凭证信息进行认证。所以你必须将该认证提供者添加到authenticationManager中,如④所示,注意key属性的设置,它和 代码清单 12中的key必须相同。如果保存在数据库中的密码使用了特殊编码,则你必须为RememberMeAuthenticationProvider配置特定的密码编码器,请参考代码清单 8进行配置。

删除Remember-Me的Cookie

站点如何提供了Remember-Me的功能,就必须同时提供能让用户手工删除Cookie的功能,以便用户在某些情况下,能够删除掉Cookie。Acegi提供了一个并不是很理想的实现:在退出系统时通过配置一个LogoutHandler清除Remember-Me的Cookie。

在上一节中,我们知道SecurityContextLogoutHandler是LogoutHandler的实现类,它负责在退出系统时删除HttpSession中的SecurityContext。LogoutHandler接口的另一个实现类是TokenBasedRememberMeServices(如前所述,它同时也实现了RememberMeServices接口)。你可以在LogoutFilter中添加TokenBasedRememberMeServices,以便用户退出系统时连带清除Remember-Me。

由于TokenBasedRememberMeServices已经在前面配置好了,这里仅需要简单地将其加入到LogoutFilter的LogoutHandler列表中即可,如下所示:

代码清单 15 applicationContext-acegi-plugin.xml:清除Remember-Me的Cookie

    
    
< bean id = " logoutFilter " class = " org.acegisecurity.ui.logout.LogoutFilter " > < constructor - arg value = " /index.jsp " /> < constructor - arg > < list > ①添加清除Remember - Me Cookie的LogoutHandler < ref bean = " rememberMeServices " /> < bean class = " org.acegisecurity.ui.logout.SecurityContextLogoutHandler " /> </ list > </ constructor - arg > < property name = " filterProcessesUrl " value = " /j_acegi_logout " /> </ bean >

想象一下,这种处理方式是否合理呢?可以说不合理得近乎点荒唐——用户在登录时选择启用Remember-Me功能,就是希望在退出系统后能够在Cookie中保留用户信息,方便后续系统的访问,现在居然在退出系统后就清除掉这个Cookie。也就是说,Remember-Me Cookie仅在用户登录到用户退出系统这段时间内有效,但这段时间我们根本不需要用到这个Cookie! httpSessionContextIntegrationFilter已经很好地通过HttpSession的转存实现了在不同请求之间共享Authentication的功能。

所以Acegi提供的这种设计,笔者认为只是一个使用范例,开发者必须编写自己的实现类以提供更有意义的实现。如在用户退出系统时,允许用户通过选择的方式决定是否删除Remember-Me的Cookie,或者专门提供一个清除Remember-Me Cookie的操作链接。

小结

使用Acegi,你就可以通过配置的方式完成应用程序的身份认证。这包括对密码进行加密的认证,使用Remember-Me,退出系统后清楚Session等在身份认证时常用的各项功能。用户认证是Acegi保护应用系统的第一步,因为只能获取操作用户的身份后,才能获取用户的权限,并根据用户权限进行程序安全控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值