Acegi Security System 是一种功能强大并易于使用的替代性方案,使您不必再为 Java 企业应用程序编写大量的安全代码。虽然它专门针对使用 Spring 框架编写的应用程序,但是任何类型的 Java 应用程序都没有理由不去使用 Acegi。
本文的主要目的是希望能够说明如何在基于Spring构架的Web应用中使用Acegi,而不是详细介绍其中的每个接口、每个类。注意,即使对已经存在的Spring应用,通过下面介绍的步骤,也可以马上享受到Acegi提供的认证和授权。
Acegi Security System 使用安全过滤器来提供企业应用程序的身份验证和授权服务。该框架提供了不同类型的过滤器,可以根据应用程序的需求进行配置。您将在本文后面了解到 安全过滤器的不同类型 ;现在,只需注意可以为如下任务配置 Acegi 安全过滤器:
-
在访问一个安全资源之前提示用户登录。
-
通过检查安全标记(如密码),对用户进行身份验证。
-
检查经过身份验证的用户是否具有访问某个安全资源的特权。
-
将成功进行身份验证和授权的用户重定向到所请求的安全资源。
-
对不具备访问安全资源特权的用户显示 Access Denied 页面。
-
在服务器上记录成功进行身份验证的用户,并在用户的客户机上设置安全 cookie。使用该 cookie 执行下一次身份验证,而无需要求用户登录。
-
将身份验证信息存储在服务器端的会话对象中,从而安全地进行对资源的后续请求。
-
在服务器端对象中构建并保存安全信息的缓存,从而优化性能。
-
当用户退出时,删除为用户安全会话而保存的服务器端对象。
- 与大量后端数据存储服务(如目录服务或关系数据库)进行通信,这些服务用于存储用户的安全信息和 ECM 的访问控制策略。
正如这个列表显示的那样,Acegi 的安全过滤器允许您执行保护企业应用程序所需的几乎任何事情。
[基础工作]
在你的Web应用的lib中添加Acegi下载包中的acegi-security.jar
[web.xml]
在web.xml配置
- < 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>限定了 FilterToBeanProxy 的 URL匹配模式 ,
- < filter-mapping >
- < filter-name > Acegi Filter Chain Proxy </ filter-name >
- < url-pattern > /* </ url-pattern >
- </ filter-mapping >
<listener>的 HttpSessionEventPublisher 用于发布 HttpSessionApplicationEvents 和 HttpSessionDestroyedEvent 事件给 spring的applicationcontext 。
- < listener >
- < listener-class >
- org.acegisecurity.ui.session.HttpSessionEventPublisher
- </ listener-class >
- </ listener >
[applicationContext-acegi-security.xml]
applicationContext-acegi-security.xml文件配置
FilterChainProxy 会按顺序来调用这些filter, 使这些filter能享用Spring ioc的功能 , CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON定义了url比较前先转为小写, PATTERN_TYPE_APACHE_ANT定义了 使用Apache ant的匹配模式
- < 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,
- basicProcessingFilter,rememberMeProcessingFilter,anonymousProcessingFilter,
- switchUserProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
- </ value >
- </ property >
- </ bean >
定义数据源为调用tomcat容器数据源 登入验证时需要获取数据源连接数据库
- < bean id = "dataSource" class = "org.springframework.jndi.JndiObjectFactoryBean" >
- < property name = "jndiName" > < value > java:/comp/env/jdbc/test </ value > </ property >
- </ bean >
认证管理 ,从数据库中读取用户信息验证身份
- < bean id = "authenticationManager" class = "org.acegisecurity.providers.ProviderManager" >
- < property name = "providers" >
- < list >
- < ref local = "daoAuthenticationProvider" />
- </ list >
- </ property >
- </ bean >
daoAuthenticationProvider
进行简单的基于数据库的身份验证。
DaoAuthenticationProvider
获取数据库中的账号密码并进行匹配,若成功则在通过用户身份的同时返回一个
包含授权信息的Authentication对象
,否则身份验证失败,抛出一个
AuthenticatiionException
。
- < bean id = "daoAuthenticationProvider" class = "org.acegisecurity.providers.dao.DaoAuthenticationProvider" >
- < property name = "userDetailsService" > < ref local = "jdbcDaoImpl" /> </ property >
- < property name = "passwordEncoder" > < ref local = "passwordEncoder" /> </ property >
- < property name = "userCache" > < ref local = "userCache" /> </ property >
- </ bean >
jdbcDaoImpl
用于在数据中获取用户信息
- < bean id = "jdbcDaoImpl" class = "com.milesup.acegi.userdetails.jdbc.JdbcDaoImpl" >
- < property name = "dataSource" > < ref bean = "dataSource" /> </ property >
- < property name = "rolePrefix" > < value > ROLE_ </ value > </ property >
- </ bean >
passwordEncoder
使用加密器对用户输入的明文进行加密。Acegi提供了三种加密器:
- 1 : PlaintextPasswordEncoder—默认,不加密,返回明文.
- 2 : ShaPasswordEncoder—哈希算法(SHA)加密
- 3 : Md5PasswordEncoder—消息摘要(MD5)加密
使用加密器对用户输入的明文进行加密 为MD5加密方式
- < bean id = "passwordEncoder" class = "org.acegisecurity.providers.encoding.Md5PasswordEncoder" />
缓存用户和资源相对应的权限信息。每当请求一个受保护资源时,
daoAuthenticationProvider
就会被调用
以获取用户授权信息
。如果每次都从数据库获取的话,那代价很高,对于不常改变的用户和资源信息来说,最好是把相关授权信息缓存起来。
userCache提供了两种实现:
NullUserCache
和
EhCacheBasedUserCache
,
NullUserCache
实际上就是不进行任何缓存,
EhCacheBasedUserCache
是使用Ehcache来实现缓功能。
- <!-- 缓存管理 -->
- < bean id = "cacheManager" class = "org.springframework.cache.ehcache.EhCacheManagerFactoryBean" />
- <!-- 缓存用户和资源相对应的权限信息 -->
- < bean id = "userCacheBackend" class = "org.springframework.cache.ehcache.EhCacheFactoryBean" >
- < property name = "cacheManager" >
- < ref local = "cacheManager" />
- </ property >
- < property name = "cacheName" >
- < value > userCache </ value >
- </ property >
- </ bean >
- < bean id = "userCache" class = "org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache" >
- < property name = "cache" > < ref local = "userCacheBackend" /> </ property >
- </ bean >
该过滤器用来处理在系统认证授权过程中抛出的异常
- < bean id = "exceptionTranslationFilter" class = "org.acegisecurity.ui.ExceptionTranslationFilter" >
- < property name = "authenticationEntryPoint" > < ref local = "authenticationProcessingFilterEntryPoint" /> </ property >
- </ bean >
一个没有进行身份验证的用户试图访问受保护的资源验证是否授权 Exception Translation Filter(ETF)
- < bean id = "authenticationProcessingFilterEntryPoint" class = "org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint" >
- < property name = "loginFormUrl" > < value > /login.jsp </ value > </ property >
- < property name = "forceHttps" > < value > false </ value > </ property >
- </ bean >
登入验证
成功进入main.jsp页面 ,失败跳转到/login.jsp?login_error=1 ,登入url为 /j_acegi_security_check, Authentication Processing Filter(APF)
authenticationFailureUrl
定义登陆失败时转向的页面
defaultTargetUrl
定义登陆成功时转向的页面
filterProcessesUrl
定义登陆请求的页面
rememberMeServices
用于在验证成功后添加cookie信息
- < bean id = "authenticationProcessingFilter" class = "org.acegisecurity.ui.webapp.AuthenticationProcessingFilter" >
- < property name = "authenticationManager" > < ref bean = "authenticationManager" /> </ property >
- < property name = "authenticationFailureUrl" > < value > /login.jsp? login_error = 1 </ value > </ property >
- < property name = "defaultTargetUrl" > < value > /main.jsp </ value > </ property >
- < property name = "filterProcessesUrl" > < value > /j_acegi_security_check </ value > </ property >
- < property name = "rememberMeServices" > < ref local = "rememberMeServices" /> </ property >
- </ bean >
- 用于处理HTTP头的认证信息,如从 < span style = "color: #0000ff;" > < strong > Spring远程协议 </ strong >
- </ span >
- (如Hessian和Burlap)或 < span style = "color: #0000ff;" > < strong > 普通的浏览器如IE,Navigator的HTTP头 </ strong >
- </ span >
- 中获取用户
- 信息,将他们转交给通过 < span style = "text-decoration: underline;" > authenticationManager </ span >
- 属性装配的认证管理器。如果认证成功,会 < span style = "text-decoration: underline;" > 将一个Authentication对象放到会话中 </ span >
- ,否则,如果认证失败,会将控制 < span style = "text-decoration: underline;" > 转交给认证入口点 </ span >
- (通过authenticationEntryPoint属性装配)
- <!-- 用于处理HTTP头的认证信息 -->
- < bean id = "basicProcessingFilter" class = "org.acegisecurity.ui.basicauth.BasicProcessingFilter" >
- < property name = "authenticationManager" > < ref local = "authenticationManager" /> </ property >
- < property name = "authenticationEntryPoint" > < ref local = "basicProcessingFilterEntryPoint" /> </ property >
- </ bean >
- <!-- 通过向浏览器发送一个HTTP401(未授权)消息,提示用户登录 -->
- < bean id = "basicProcessingFilterEntryPoint" class = "org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint" >
- < property name = "realmName" > < value > Milesup Realm </ value > </ property >
- </ bean >
注销 退出验证 跳转到/login.jsp
- < bean id = "logoutFilter" class = "org.acegisecurity.ui.logout.LogoutFilter" >
- < constructor-arg value = "/login.jsp" /> < constructor-arg >
- < list >
- < bean class = "org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
- </ list >
- </ constructor-arg >
- </ bean >
经过 投票 机制来决定是否可以访问某一资源( URL 或 方法 )。allowIfAllAbstainDecisions为false时如果有一个或以上的decisionVoters投票通过,则授权通过。可选的决策机制有ConsensusBased和UnanimousBased
roleVoter
必须是以rolePrefix设定的value开头的权限才能进行投票,如 ROLE_
- < bean id = "roleVoter" class = "org.acegisecurity.vote.RoleVoter" >
- < property name = "rolePrefix" >
- < value > ROLE_ </ value >
- </ property >
- </ bean >
- <!-- 组件管理授权过程 决策管理器-->
- < bean id = "httpRequestAccessDecisionManager" class = "org.acegisecurity.vote.AffirmativeBased" >
- < property name = "allowIfAllAbstainDecisions" > < value > false </ value > </ property >
- < property name = "decisionVoters" >
- < list >
- < ref bean = "roleVoter" />
- </ list >
- </ property >
- </ bean >
过滤器安全拦截器 是否认证,是否有权限访问受保护的资源
在执行转向url前检查 objectDefinitionSource 中设定的用户权限信息。首先, objectDefinitionSource 中定义了访问URL需要的属性信息(这里的属性信息仅仅是标志,告诉 accessDecisionManager 要用哪些voter来投票)。然后, authenticationManager 掉用自己的provider来对用户的认证信息进行校验。最后,有投票者根据用户持有认证和访问url需要的属性,调用自己的voter来投票,决定是否允许访问。
- < bean id = "filterInvocationInterceptor" class = "org.acegisecurity.intercept.web.FilterSecurityInterceptor" >
- < property name = "authenticationManager" > < ref bean = "authenticationManager" /> </ property >
- < property name = "accessDecisionManager" > < ref local = "httpRequestAccessDecisionManager" /> </ property >
- < property name = "objectDefinitionSource" >
- < value >
- PATTERN_TYPE_APACHE_ANT
- / user.jsp = ROLE_ADMIN
- / admin = ROLE_ADMIN
- </ value >
- </ property >
- </ bean >
[login.jsp]
- < form action = "/j_acegi_security_check" method = "post" >
- < table >
- < tr >
- < td width = "140" height = "22" align = "right" > 用户名 </ td >
- < td width = "47%" align = "left" > < input value = "${lastUserName}" name = "j_username" type = "text" class = "inputstyle" id = "j_username" > </ td >
- < td align = "left" nowrap > < div class = "loginmeon" > </ div > </ td >
- </ tr >
- < tr >
- < td width = "140" height = "22" align = "right" > 密 码 </ td >
- < td width = "47%" align = "left" > < input name = "j_password" type = "password" class = "inputstyle" id = "j_password" size = "21" > </ td >
- </ tr >
- < tr >
- < td height = "60" colspan = "3" align = "center" valign = "middle" >
- < input type = "submit" name = "imageField2" value = "登入" > </ td >
- </ tr >
- </ table >
- </ td >
- </ tr >
- </ table >
- </ td >
- </ tr >
- </ table >
- </ form >
[JdbcDaoImpl.java]
- public class JdbcDaoImpl extends org.acegisecurity.userdetails.jdbc.JdbcDaoImpl {
- private String anonymousRoleName = "ROLE_ANONYMOUS" ;
- private Log logger = LogFactory.getLog(JdbcDaoImpl. class );
- private PreparedStatement userPstmt;
- private PreparedStatement rolePstmt;
- public UserDetails loadUserByUsername(String userName)
- throws UsernameNotFoundException, DataAccessException {
- UserDetails user = findUserByName(userName);
- if (user == null ) {
- throw new UsernameNotFoundException( "User not found" );
- }
- return user;
- }
- private UserDetails findUserByName(String userName) {
- Connection connection = null ;
- ResultSet rsUser = null ;
- ResultSet rsRole = null ;
- UserDetails user = null ;
- String logonName = null ;
- String password = null ;
- String roleName = null ;
- int status = - 1 ;
- boolean enabled = false ;
- Vector roles = null ;
- GrantedAuthority[] rolesArray = null ;
- try {
- connection = getDataSource().getConnection();
- userPstmt = connection
- .prepareStatement( "select * from users where user_NAME=?" );
- userPstmt.setString( 1 , userName);
- rsUser = userPstmt.executeQuery();
- if (rsUser.next()) {
- logonName = rsUser.getString( "USER_NAME" );
- password = rsUser.getString( "PASSWORD" );
- status = rsUser.getInt( "STATUS" );
- if (status == 1 )
- enabled = true ;
- } else {
- return null ;
- }
- rolePstmt = connection
- .prepareStatement( "SELECT ROLE.NAME Role FROM ROLE, users_ROLES, users WHERE ROLE.ID= user_ROLES.FK_ROLES and users.user_NAME=?" );
- rolePstmt.setString( 1 , userName);
- rsRole = rolePstmt.executeQuery();
- roles = new Vector();
- while (rsRole.next()) {
- roleName = getRolePrefix() + rsRole.getString( "Role" );
- roles.add( new GrantedAuthorityImpl(roleName));
- }
- rolesArray = new GrantedAuthority[roles.size() + 1 ];
- int index = 0 ;
- for (index = 0 ; index < roles.size(); index++)
- rolesArray[index] = (GrantedAuthority) roles.get(index);
- rolesArray[index] = new GrantedAuthorityImpl(anonymousRoleName);
- user = new User(logonName, password, enabled, true , true , true ,
- rolesArray);
- } catch (SQLException e) {
- logger.fatal( "" , e);
- } finally {
- try {
- rsRole.close();
- rsUser.close();
- userPstmt.close();
- rolePstmt.close();
- connection.close();
- } catch (SQLException sqlx) {
- logger.fatal( "" , sqlx);
- } catch (NullPointerException x) {
- logger.fatal( "" , x);
- }
- }
- return user;
- }
- }