第六章. 通用认证服务
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"/>
6.4. 认证标签库
AuthenticationTag
只是用来把
principal
的
Authentication.getPrincipal()
对象的属性显示到
web
页面。
下面的
JSP
片段展示了如何使用
AuthenticationTag
:
java 代码
- <authz:authentication operation="username"/>
这个标签将会显示
pricipal
的名字。这里我们假设
Authentication.getPrincipal()
是一个
UserDetails
对象,这在使用典型的
DaoAuthenticationProvider
时候的一般状况。