原创作者: leondu
阅读:4827次
评论:1条
更新时间:2011-05-26
第六章. 通用认证服务
6.1. Mechanisms, Providers 和 Entry Points
如果你使用Acegi Security提供的认证方法,那么通常你需要配置一个web filter,一个AuthenticationProvider
以及AuthenticationEntryPoint。在本节我们将要浏览一个示例应用,它需要支持基于form的认证(例如提供给用户登录的HTML页面)以及基础认证(例如web service或者类似的可以访问受保护资源)。
在web.xml中,这个应用需要一个单独的Acegi Security filter来使用FilterChainProxy。几乎所有的Acegi Security应用都有一个类似的项,看起来象下面这样:
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>
- <filter-name>Acegi Filter Chain Proxy</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
上述声明将使每个web请求都要经过Acegi Security的FilterChainProxy。正如在本手册的filter那节中所说,FilterChainProxy是一个通用类,它使得web请求按照URL模式被发送到不同的filter。那些被委派的filter是由application context管理的,因此它们可以享受依赖注射的好处。我们来看看在你的application context中FilterChainProxy的定义会是什么样的:
xml 代码
- <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,securityContextHolderAwareRequestFilter,</value>
- </property>
- </bean>
在内部,Acegi Security会使用PropertyEditor来将上述XML片段中的字符串转化为一个FilterInvocationDefinitionSource对象。在这个阶段需要注意的是,一系列的filter会按照定义的顺序运行,并且这些filter实际就是application context中的bean的<bean id>。所以,在我们的例子中,会在application context出现另外一些bean,它们会被命名为httpSessionContextIntegrationFilter, logoutFilter 等。Filter出现的顺序会在手册中filter那一节讨论,虽然上述的例子中它们是正确的。
在我们的例子中,我们使用了AuthenticationProcessingFilter和BasicProcessingFilter。它们分别对应了基于form的认证和BASIC HTTP header-based认证的“认证机制”(我们在手册的前面部分讨论了认证机制扮演的角色)。如果你既不使用form也不使用BASIC认证,就不需要定义这些bean了。取而代之的是你要定义对应你所需要的认证环境的filter,例如DigestProcessingFilter 或者CasProcessingFilter。请对照手册中对应的章节来了解如何配置这些认证机制。
让我们回忆一下,在HttpSessionContextIntegrationFilter中保存了每个HTTP session调用中的SecurityContext。这意味着认证机制只会在principal最初尝试认证的时候被使用一次。在余下的时间内,认证机制只是静静的待在那里,将请求发往filter链中的下一个filter。这个基于实际的需求源于这样的一个事实,很少有认证实现在每一个,每一次的调用的时候都会进行认证(BASIC认证是一个值得注意的例外),但是如果一个pricipal在最初的认证步骤之后帐号被取消了,或者被禁用了,或者被修改了(例如GrantedAuthority[]中增加或者减少)会怎么样呢?让我们来看看现在这些情况是如何处理的。
前面已经介绍了安全对象的主要认证provider AbstractSecurityInterceptor。这个类需要能够访问一个AuthenticationManager。它同时有个可选配置可以设定一个认证对象每次安全对象调用的时候是否需要重新认证。如果Authentication.isAuthenticated()返回true,那么它默认在SecurityContextHolder中的认证对象是已认证的。这样做对于提高性能是非常好的,但是对于即时的认证验证是不理想的。在这样的情况下你可能需要将AbstractSecurityInterceptor.alwaysReauthenticate属性设置为true。
你可能会问自己“这个AuthenticationManager是什么?”我们之前没有见过它,但是我们曾经讨论过AuthenticationProvider的概念。非常简单,AuthenticationManager负责在AuthenticationProvider链之间传递请求。它非常象我们之前讨论过的filter链,虽然有一些不同。Acegi Security只提供了一个AuthenticationManager实现,因此让我们看看对于我们这章的例子,它是如何配置的:
xml 代码
- <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
- <property name="providers">
- <list>
- <ref local="daoAuthenticationProvider"/>
- <ref local="anonymousAuthenticationProvider"/>
- <ref local="rememberMeAuthenticationProvider"/>
- </list>
- </property>
- </bean>
在这个时候,可能值得提到的是你的认证机制(通常是filter)也被注入了一个AuthenticationManager的引用。所以和认证机制都会使用上述的ProviderManager来轮询一系列的AuthenticationProvider。
在我们例子中有三个provider。它们按照上述的顺序调用(使用list而不是set来显示是按照顺序调用的),每个provider都能够尝试认证,或者仅仅返回一个null来跳过认证。如果所有的实现都返回null,ProviderManager会抛出一个相应的异常。如果你想了解更多chaining providers的信息,请参阅ProviderManager的JavaDoc。
authentication mechanism使用的那些provider有时候是可以互换的,而有时候它们又依赖于特定的authentication mechanism。例如,DaoAuthenticationProvider只需要一个基于字符串的用户名和密码。若干个认证机制会产生基于字符串的用户名和密码的集合,包括(但不限于)BASIC 和 form 认证。同时,有些认证机制会产生一个只能和特定类型的AuthenticationProvider交互的认证请求对象。一个这种一对一映射的例子是JA-SIG CAS,它使用service ticket的概念,只能被Common Authentication Services CasAuthenticationProvider认证。一个更加深入的一对一映射的例子是LDAP认证机制,它只能由LdapAuthenticationProvider处理。这种特定的对应关系在每个类的JavaDoc以及在本手册的特定认证方法章节中有详细说明。你不用担心这些实现的细节,因为如果你忘记注册一个合适的provider,你在尝试认证时只会收到一个ProviderNotFoundException异常。
当你在FilterChainProxy中正确配置了认证机制,并且确保注册了对应的AuthenticationProvider,你的最后一步是配置一个AuthenticationEntryPoint。回忆一下早先我们讨论过的ExceptionTranslationFilter的角色,当一个基于HTTP的请求收到一个HTTP头或者一个HTTP重定向以开始认证时它被使用。继续我们早先的例子:
xml 代码
- <bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
- <property name="authenticationEntryPoint"><ref
- local="authenticationProcessingFilterEntryPoint"/></property>
- <property name="accessDeniedHandler">
- <bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
- <property name="errorPage" value="/accessDenied.jsp"/>
- </bean>
- </property>
- </bean>
- <bean id="authenticationProcessingFilterEntryPoint"
- class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
- <property name="loginFormUrl"><value>/acegilogin.jsp</value></property>
- <property name="forceHttps"><value>false</value></property>
- </bean>
注意到ExceptionTranslationFilter需要两个协作者。第一个AccessDeniedHandlerImpl,使用一个RequestDispatcher导向显示特定的访问拒绝的错误页面。我们使用forwad所以SecurityContextHolder中仍然保留principal的详细信息,这些对于显示给用户来说是有用的(在Acegi Security的老版本中,我们依赖rervlet容器来处理403错误信息,它缺乏这个有用的上下文信息)。AccessDeniedHandlerImpl同时将会将HTTP头设置为403,它是访问拒绝的正式错误代码。至于AuthentionEntryPoint,这里设置如果一个未受认证的principal尝试执行一个受保护的操作时,我们需要执行那些动作。因为在我们的例子中要使用基于form的认证,因此我们设定AuthenticationProcessinFilterEntryPoint以及登录页面的URL。你的应用系统通常只需要一个entry point,并且大多数的认证方法都定义了自己特有的AuthenticationEntryPoint。每个认证方式所对应的特定entry point的详细情况会在本手册特定的认证方法章节中介绍。
6.2. UserDetails 和 Associated Types
正如在第一部分中提到的,大多数认证provider要用到UserDetails 和UserDetailsService 接口。后面那个接口只包含一个方法:
java 代码
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException,
- DataAccessException;
返回值UserDetails是一个接口,它提供了若干个getter保证返回非null值,例如用户名,密码,授予的权限以及用户是启用还是禁用状态。大部分认证provider都会使用一个,即使它在认证判断过程中实际并不使用用户名和密码。通常这些provider只会使用返回的UserDetails中的GrantedAuthority[]信息,因为有些系统(例如LDAP 或 X509 或 CAS)已经承担了实际的身份验证的责任。
Acegi Security提供了一个UserDetails的实体类实现-User。Acegi Security用户需要确定什么时候实现UserDetailsService以及返回什么样的UserDetails实体类。通常,直接使用User类或者继承User类就可以了,尽管有一些特殊情况(例如 object relational mappers),需要用户从头写他们自己的UserDetails实现。这种情况也时有发生,用户只要返回他们正常的代表系统用户的领域对象就可以了。特别是UserDetails经常被用来存储额外的principal相关属性(例如他们的电话号码以及email地址),这样它们可以很容易被web视图使用。
特定的UserDetailsService实现起来是很简单的,它应该很容易由用户来选择持久化策略来获取认证信息。说到这里,Acegi Security确实包含了一些有用的基础实现,下面让我们看一下。
6.2.1. In-Memory 认证
虽然用户可以创建一个定制的UserDetailsService实现来从一个持久化引擎中获取信息,很多应用不需要这种复杂性。特别是如果你正在进行快速原型开发或者刚开始集成Acegi Security,当你不需要花费时间来进行数据库配置或者写UserDetailsService的实现。这种情况之下,你有一个简单的选择,就是配置InMemoryDaoImpl实现。
xml 代码
- <bean id="inMemoryDaoImpl" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
- <property name="userMap">
- <value>
- marissa=koala,ROLE_TELLER,ROLE_SUPERVISOR
- dianne=emu,ROLE_TELLER
- scott=wombat,ROLE_TELLER
- peter=opal,disabled,ROLE_TELLER
- </value>
- </property>
- </bean>
在上面的例子中,userMap属性包含了每个用户的用户名,密码,一个授权列表以及一个可选的启用/禁用关键词。使用逗号分隔。用户名必须在等号的左侧,密码必须在等号右侧第一个出现。启用和禁用关键词(大小写敏感)可以出现在第二个或者之后任意位置。剩余的字符串被看作是授予的权限,这些权钱被创建为GrantedAuthorityImpl对象(仅供参考-大多数的应用不需要自定义的GrantedAuthority实现,所以使用默认的实现就可以了)。注意如果一个用户没有密码及或没有被授予权限,该用户不会在in-memory 认证库中创建。
InMemoryDaoImpl也提供了一个setUserProperties(Properties)方法,可以允许你用另一个Spring的配置好的bean或者一个外部的properties文件来实例化属性。你可能要使用Spring的PropertiesFactoryBean,它在加载外部属性文件的时候非常有用。这个setter可能对于有大量用户的应用,或者开发期配置变更有所助益,但是不要指望使用整个数据库来处理认证细节。
6.2.2. JDBC 认证
也包括了一个从JDBC数据源获取认证信息的UserDetailsService。使用Spring内部的JDBC,避免了仅仅为了存储用户信息而使用复杂的对象关系Common Authentication Services 映射(ORM)。如果你确实使用ORM工具,你可能要写一个定制的UserDetailsService来重用你已经创建的映射文件。回到JdbcDaoImpl,下面是一个配置的例子:
xml 代码
- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
- <property name="driverClassName"><value>org.hsqldb.jdbcDriver</value></property>
- <property name="url"><value>jdbc:hsqldb:hsql://localhost:9001</value></property>
- <property name="username"><value>sa</value></property>
- <property name="password"><value></value></property>
- </bean>
- <bean id="jdbcDaoImpl" class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
- <property name="dataSource"><ref bean="dataSource"/></property>
- </bean>
你可能要修改上述的DriverManagerDataSource来使用不同的关系数据库管理系统。你还可以使用从JNDI获取的全局数据源,如上的常规Spring选项。不论是使用什么数据库以及如何获取数据源,必须使用一个按照dbinit.txt中写明的数据库模式。你可以从Acegi Security网站下载这个文件。
如果你的默认数据库模式不能满足需要,JdbcDaoImpl提供了两个属性允许定制SQL语句。如果需要进一步定制,你可以继承JdbcDaoImpl。请参考JavaDocs获取详情,不过请注意这个类并不是为了复杂的自定义继承而写的。如果你的需求比较复杂(例如数据库结构比较特殊或者需要返回一个特定的UserDetails实现),那么你最好写自己的UserDetailsService实现。Acegi Security提供的基础实现只是为了典型场景,并没有提供无限的配置灵活性。
6.3. 并行Concurrent Session 处理
Acegi Security能够限定次数防止一个principal多次并行认证到同一个应用。许多ISV利用这一点来加强授权管理,网管也喜欢这个特性因为可以防止一个用户名被重复使用。例如,你可以限制“Batman”用户从两个不同的session登录系统。
使用并行session支持,你需要在web.xml中增加如下内容:
xml 代码
- <listener>
- <listener-class>org.acegisecurity.ui.session.HttpSessionEventPublisher</listener-class>
- </listener>
而且,你需要在中FilterChainProxy增加org.acegisecurity.concurrent.ConcurrentSessionFilter to your FilterChainProxy。ConcurrentSessionFilter需要两个属性,sessionRegistry用来指向一个SessionRegistryImpl实例,expiredUrl指向一个session实效时显示的页面。
当一个HttpSession开始或者结束的时候web.xml HttpSessionEventPublisher发送一个ApplicationEvent到Spring ApplicationContext。这很关键,因为它确保session终止的时候SessionRegistryImpl会收到通知。
你还要装配ConcurrentSessionControllerImpl并在ProviderManager中引用:
xml 代码
- <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
- <property name="providers">
- <!-- your providers go here -->
- </property>
- <property name="sessionController"><ref bean="concurrentSessionController"/></property>
- </bean>
- <bean id="concurrentSessionController"
- class="org.acegisecurity.concurrent.ConcurrentSessionControllerImpl">
- <property name="maximumSessions"><value>1</value></property>
- <property name="sessionRegistry"><ref local="sessionRegistry"/></property>
- </bean>
- <bean id="sessionRegistry" class="org.acegisecurity.concurrent.SessionRegistryImpl"/>
AuthenticationTag只是用来把principal的Authentication.getPrincipal()对象的属性显示到web页面。
下面的JSP片段展示了如何使用AuthenticationTag:
java 代码
- <authz:authentication operation="username"/>
这个标签将会显示pricipal的名字。这里我们假设Authentication.getPrincipal()是一个UserDetails对象,这在使用典型的DaoAuthenticationProvider时候的一般状况。
1 楼 net.xiao 2009-06-12 15:49