身份认证管理
使用Acegi保护应用程序的第一步是根据用户提供的认证信息进行身份认证,以确定用户的身份获取对应的权限信息准备好Authentication。通过认证的Authentication拥有权限信息,它是Acegi进行后续安全对象访问安全控制的依据。
基于内存存储用户信息的身份认证
applicationContext-acegi-plugin.xml
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> <property name="filterInvocationDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT /**=authenticationProcessingFilter </value> </property> </bean> <bean id=" authenticationProcessingFilter " class="org.acegisecurity.util.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"/> </bean>
添加登录页面index.jsp:
<%@ 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/>
<input type="submit" value="登录"/>
</form>
applicationContext-acegi-plugin.xml : 认证管理器的配置
<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> <!—处理过滤器的URL --> <property name="filterProcessesUrl" value="/j_acegi_security_check"/> <!--认证成功后转向的URL --> <property name="defaultTargetUrl" value="/main.jsp"/> <!--认证失败后转向的URL --> <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> <!--使用基于DAO的认证提供者提供认证服务--> <ref local="daoAuthenticationProvider" /> </list> </property> </bean> <bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"> <!--注入根据用户名获取系统中真实UserDetails对象的服务类 --> <property name="userDetailsService" ref="userDetailsService" /> </bean> <bean id="userDetailsService" class="org.acegisecurity.userdetails.jdbc.InMemoryDaoImpl"> <property name="userMap"> <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>
Acegi提供了不同的AuthenticationProvider的实现,如:
- DaoAuthenticationProvider 从Dao类负责读取用户信息验证身份
- AnonymousAuthenticationProvider 匿名用户身份认证
- RememberMeAuthenticationProvider 已存cookie中的用户信息身份认证
- AuthByAdapterProvider 使用容器的适配器验证身份
- CasAuthenticationProvider 根据Yale中心认证服务验证身份, 用于实现单点登陆
- JaasAuthenticationProvider 从JASS登陆配置中获取用户信息验证身份
- RemoteAuthenticationProvider 根据远程服务验证用户身份
- RunAsImplAuthenticationProvider 对身份已被管理器替换的用户进行验证
- X509AuthenticationProvider 从X509认证中获取用户信息验证身份
- TestingAuthenticationProvider 单元测试时使用
DaoAuthenticationProvider 通过UserDetailsService完成UserDetails的获取工作,根据存储用户信息媒介的不同,Acegi提供了两个UserDetailsService的实现类:
- InMemoryDaoImpl:该实现类负责从内在中获取用户的信息
- JdbcDaoImpl:该实现类从数据库中获取用户的信息
如果用户数比较多,在Spring中直接进行配置未免不太雅观,这时,可以将用户信息转移到一个属性文件中,并通过userProperties进行加载,则需对userDetailsService稍做修改
<bean id="userDetailsService" class="org.acegisecurity.userdetails.jdbc.InMemoryDaoImpl"> <property name="userProperties"> <bean class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="location" value="/WEB-INF/users.properties"/> </bean> </property> </bean>
基于数据库存储用户信息的认证
applicationContext-acegi-plugin.xml :
<bean id="userDetailsService" class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl"> <!—数据源--> <property name="dataSource" ref="dataSource" /> <property name="usersByUsernameQuery"> <value> <!--根据用户名查询用户的SQL语句--> SELECT username,password,status FROM t_user WHERE username = ? </value> </property> <property name="authoritiesByUsernameQuery"> <value> <!--根据用户名查询用户权限记录的SQL语句--> SELECT u.username,p.priv_name FROM t_user u,t_user_priv p WHERE u.user_id =p.user_id AND u.username = ? </value> </property> </bean>
应该说JdbcDaoImpl还不是非常实用的UserDetailsService实现类,因为用户对象除包含用户名/密码、是否激活、权限等信息外,还经常需要包含一些诸如email、telephone等到业务相关的信息,所以我们往往需要通过实现UserDetailsService接口提供自己的的实现类来完成这些工作。
具体可以参照:Spring Security 2 配置精讲: http://www.iteye.com/topic/319965
密码加密问题
在获取UserDetails后,DaoAuthenticationProvider要做的工作是比较Authentication 和UserDetails的匹配关系并给出认证成功或失败的认证结果。下面是两个关键接口:
- org.acegisecurity.providers.encoding.PasswordEncoder
- org.acegisecurity.providers.dao.SaltSource
PasswordEncoder完成两件工作:
- 对明文的密码(Authentication#getCredentials())进行编码
- 对处于非对称状态(一个是加密的,另一个是明文的)
PasswordEncoder进行密码比较时,需要使用到一个SaltSource,它代表一个“加密盐”,对用户提供的密码进行加密时采用的加密盐必须和系统中保存的用户加密密码所采用的加密盐相同。它有两个接口方法:
String encodePassword(String rawPass, Object salt) //对原始未加密的密码通过一定的算法进行加密运算
Boolean isPasswordValid(String encPass, String rawPass, Object salt) //通过算法判断待认证用户所提供的密码是否有效
几种常的PasswordEncoder实现类,密码编码器
- Md5PasswordEncoder 使用MD5算法加密
- ShaPasswordEncoder 使用SHA算法加密
- LdapShaPasswordEncoder 使用LDAP SHA 和平SSHA算法加密
- PlaintextPasswordEncoder 不加密
SaltSource接口公有一个Object gestalt(UserDetails user)方法,它有两个实现类:
org.acegisecurity.providers.dao.salt.ReflectionSaltSource:允许用户在UserDetails中提供一个代表加密盐的属性
org.acegisecurity.providers.dao.salt.SystemWideSaltSource:该实现类不允许不同用户采用各自的加密盐,它采用全局范围统一的加密盐。
applicationContext-acegi-plugin.xml :
<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"> <!--注入根据用户名获取系统中真实UserDetails对象的服务类 --> <property name="userDetailsService" ref="userDetailsService" /> <property name="passwordEncoder"> <bean class="org.acegisecurity.providers.encoding.Md5PasswordEncoder"/> </property> <property name="saltSource"> <bean class="org.acegisecurity.providers.dao.salt.SystemWideSaltSource"> <property name="systemWideSalt" value="ccd1010"/> </bean> </property> </bean>
成功登录系统的后置处理
一般的业务系统在用户登录成功后,需要在数据库中记录一条相应的用户登录日志。Acegi会产生一个AuthenticationSuccessEvent事件, 该事件是org.springframework.context.ApplicationEvent的子类,所以它是一个Spring容器事件。
package com.ccd.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容器中声明这个监听器,仅需要一行就可以了:
<bean class=”com.ccd.service.LoginSuccessListener”/>
在多个请求之间共享SecurityContext
Acegi通过HttpSessionContextIntegrationFilter使SecurityContext在Session级别中共享,当一个请求到达时,它尝试从Session中获取用户关联的SecurityContext并将其放入到SecurityContextHolder中,当请求结束时,HttpSessionContextIntegrationFilter又将SecurityContext转存到HttpSession中。这样,Acegi就通过HttpSessionContextIntegrationFilter将SecurityContext对象在请求级的SecurityContextHolder和Session级的HttpSession中摆渡,从而保证SecurityContext可以在多个请求之间共享。注意,此filter必须于其他Acegi Filter前使用。
applicationContext-acegi-plugin.xml :
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> <property name="filterInvocationDefinitionSource"> <value> … /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter </value> </property> </bean> <!—通过HttpSession转存SecurityContext的过滤器 --> <bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter" />
退出系统的后置处理
SecyrityContext保存在HttpSession中,当用户退出系统时必须清除之,否同要等到Session过期后才会被清除,造成额外的内存消耗。Acegi为完成一系列由退出系统引发的操作,专门提供了一个退出过滤器:org.acegisecurity.ui.logout.LogoutFilter。
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> <list> <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" /> </list> </constructor-arg> <!—退出系统后转向的URL --> <constructor-arg value="/index.jsp" /> <!—用于响应退出系统请求的URL--> <property name="filterProcessesUrl" value="/j_acegi_logout" /> </bean>
配置一个退出系统的操作链接
<A href= "<c:url value="/j_acegi_logout"/>">退出系统</A>
实施Remember-Me认证
很多网站的登录页面都提供了一个类似于“两周内不用再登录”、“记住我的账号”等功能,其原理是在用户登录成功后使用客气端浏览器的Cookie记录用户登录信息,当下次再访问相同站点时,直接从Cookie中取得用户登录信息并进行自动登录。
如果说HttpSessionContextIntegrationFilter通过HttpSession使Authentication获得跨请求共享的能力,那么Remember-Me则通过Cookie使Authentication获得跨多个Session的能力。
Org.acesecurity.ui.rememberme.RememberMeServices是Remember-Me方案中最关键的一个接口,有几个接口方法:
void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthenctication)
void loginFail(HttpServletRequest request, HttpServletResponse response)
void autoLogin(HttpServletRequest request, HttpServletResponse response)
loginSuccess() 和loginFail()方法的调用已经编制到Acegi的AbstractProcessingFilter抽象过滤容器中,这意味着任何注入了RememberMeServices实例的过滤器都会以适合的方式调用这两个方法。而autoLogin()方法则通过RememberMeProcessingFilter进行调用,当RememberMeProcessingFilter发现SecurityContextHolder不存在有效的Authentication时,autoLogin()就会被执行。
Acegi为RememberMeServices接口提供了两个实现类:
- NullRememberMeServices:不做任何有意义的事情
- TokenBasedRememberMeServices:基于凭证(用户名/密码)的实现类
<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<!--注入一个RememberMeServices -->
<property name="rememberMeServices" ref="rememberMeServices"/>
</bean>
<bean id="rememberMeServices"
class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
<!--Cookie有效时间,单位为秒 -->
<property name="tokenValiditySeconds" value="432000"/>
<!--Cookie中的键值 -->
<property name="key" value="ccd1010"/>
</bean>
loginSuccess()将用户名/密码进行编码后写到客户端Cookie中
base64(username+":"+expirationTime+":"+md5Hex(username+":"+expirationTime+":"+password+":"+key))
index.jsp添加是否使用Remember-Me功能的控制参数
<%@ 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/>
<input type="checkbox" name="_acegi_security_remember_me">5天内不用再登录
<input type="submit" value="登录"/>
</form>
根据Remember-Me 进行自动登录
applicationContext-acegi-plugin.xml :
<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 "> <!--注入一个RememberMeServices --> <property name="rememberMeServices" ref="rememberMeServices"/> <property name="authenticationManager" ref="authenticationManager"/> </bean> <bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices"> <!--Cookie有效时间,单位为秒 --> <property name="tokenValiditySeconds" value="432000"/> <!--Cookie中的键值 --> <property name="key" value="ccd1010"/> <property name="userDetailsService" ref=" userDetailsService "/> </bean> <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager"> <property name="providers"> <list> <!—使用基于DAO的认证提供者提供认证服务 --> <ref local="daoAuthenticationProvider" /> <!—负责对基于Cookie的用户凭证信息进行认证 --> <bean class="org.acegisecurity. providers.rememberme. RememberMeAuthenticationProvider" <property name="key" value="ccd1010"/> </bean> </list> </property> </bean>
删除Remember-Me 的Cookie
Acegi 提供了一个并不是很理想的实现:在退出系统时通过配置一个LogoutHandler 清除Remember-Me的Cookie。
applicationContext-acegi-plugin.xml :
<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter"> <!—退出系统前需要执行的操作 --> <constructor-arg> <list> <!—添加清除Remember-Me Cookie的LogoutHandler --> <ref bean="rememberMeServices"/> <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" /> </list> </constructor-arg> <!—退出系统后转向的URL --> <constructor-arg value="/index.jsp" /> <!—用于响应退出系统请求的URL--> <property name="filterProcessesUrl" value="/j_acegi_logout" /> </bean>
PS:本文主要摘自《精通Spring 2.x ——企业应用开发详解》