身份认证管理
使用 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 >
<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 >
<%@ 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 >
<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 >
<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 >
<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) //通过算法判断待认证用户所提供的密码是否有效
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 >
<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+”]成功登录日志...”);
- }
- }
- }
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" />
<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 >
<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 认证