前面介绍了DaoAuthenticationProvider
,它可以从数据库中读取用户信息,同样也可以从一个用户属性文件中读取,下一篇文章中我们在介绍如何从数据库中读取用户信息,当然还会涉及到更深入的东西,比如根据自己系统的需要自定义
UserDetails
和UserDetailsService,这个只是让你对整个系统有个简单的了解,所以我们使用
用户属性文件(
users.properties)
来存储用户信息:
1
admin
=
admin,ROLE_SUPERVISOR
2
3
user1
=
user1,ROLE_USER
4
5
user2
=
user2,ROLE_USER
6
7
user3
=
user3,disabled,ROLE_USER
配置
userDetailsService
:
1
<
bean
id
="userDetailsService"
2
3
class
="org.springframework.security.userdetails.memory.InMemoryDaoImpl"
>
4
<
property
name
="userProperties"
>
5
<
bean
class
="org.springframework.beans.factory.config.PropertiesFactoryBean"
6 p:location
="/WEB-INF/users.properties"
/>
7
</
property
>
8
</
bean
>
InMemoryDaoImpl
类是
UserDetailsService
接口的一个实现,它从属性文件里读取用户信息,Spring Security使用一个属性编辑器将用户信息为我们组织成一个org.springframework.security.userdetails.memory.
UserMap
类的对象,我们也可以直接为它提供一个用户权限信息的列表,详见applicationContext-security.xml配置文件。
UserMap
字符串的每一行都用键值对的形式表示,前面是用户名,然后是等号,后面是赋予该用户的密码/权限等信息,它们使用逗号隔开。比如:
1
admin
=
admin,ROLE_SUPERVISOR
定义了一个名为
admin
的用户登录密码为
admin
,该用户拥有
ROLE_SUPERVISOR
权限,再如users.properties
文件中配置的
名为
user3
的用户登录密码为
user3
,该用户拥有
ROLE_USER
权限,disabled定义该用户不可用,为被激活(
UserDetails
的
isEnabled
方法)。
即使是系统的开发者或者说是最终用户,都不应该看到系统中有明文的密码。所以,Spring Security考虑的还是很周到的,为我们提供的密码加密的功能。正如你在Dao认证提供者(DaoAuthenticationProvider
)中看到的,passwordEncoder
属性配置的就是一个密码加密程序(密码编码器)。这里我们使用MD5加密,可以看
配置文件中的scott用户,你还能看出他的密码是什么吗?当然这里只是演示功能,其它用户还是没有改变,你可以自己试试。系统为我们提供了一些常用的密码编码器(这些编码器都位于org.springframework.secu rity.providers.encoding包下):
PlaintextPasswordEncoder(默认)
――不对密码进行编码,直接返回未经改变的密码;
Md4PasswordEncoder ――
对密码进行消息摘要(MD4)编码;
Md5PasswordEncoder ――
对密码进行消息摘要(MD5)编码;
ShaPasswordEncoder ――
对密码进行安全哈希算法(SHA)编码。
你可以根据需要选择合适的密码编码器,你也可以
设
置
编码
器的
种子源(salt source)
。一个
种
子源
为编码
提供
种
子
(salt),或者称
编码
的密
钥,这里不再赘述。
这里附加介绍了不少东西,希望你还没有忘记在AuthenticationManager
(
认证管理器)中还配置了一个名为sessionController的Bean,这个Bean可以阻止用户在进行了一次成功登录以后在进行一次成功的登录。在
applicationContext-security.xml
配置文件添加
sessionController
的配置:
1
<
bean
id
="concurrentSessionController"
2
3
class
="org.springframework.security.concurrent.ConcurrentSessionControllerImpl"
4
p:maximumSessions
="1"
5
p:exceptionIfMaximumExceeded
="true"
6
p:sessionRegistry-ref
="sessionRegistry"
/>
7
<
bean
id
="sessionRegistry"
8
9
class
="org.springframework.security.concurrent.SessionRegistryImpl"
/>
maximumSessions
属性配置了只允许同一个用户登录系统一次,exceptionIfMaximumExceeded属性配置了在进行第二次登录是是否让第一次登录失效。这里设置为true不允许第二次登录。要让此功能生效,我们还需要在web.xml文件中添加一个监听器,以让Spring Security能获取Session的生命周期事件,配置如下:
1
<
listener
>
2
<
listener-class
>
3
org.springframework.security.ui.session.HttpSessionEventPublisher
4
</
listener-class
>
5
</
listener
>
HttpSessionEventPublisher
类实现javax.servlet.http.HttpSessionListener接口,在Session被创建的时候通过调用ApplicationContext的publishEvent(ApplicationEvent event)发布HttpSessionCreatedEvent类型的事件,HttpSessionCreatedEvent类继承自org.springframework.context.ApplicationEvent类的子类 HttpSessionApplicationEvent抽象类。
concurrentSessionController
使用sessionRegistry来完成对发布的Session的生命周期事件的处理,org.springframework.security.concurrent.
SessionRegistryImpl
(实现了SessionRegistry接口), SessionRegistryImpl类还实现了Spring Framework 的事件监听org.springframework.context.Application Listener接口,并实现了该接口定义的onApplicationEvent(ApplicationEvent event)方法用于处理Applic ationEvent类型的事件,如果你了解Spring Framework的事件处理,那么这里你应该可以很好的理解。
认证管理器到此介绍完毕了,认证过程过滤器也介绍完了,接下来我们继续介绍过滤器链的下一个过滤器
securityContextHolderAwareRequestFilter
:
1
<
bean
id
="securityContextHolderAwareRequestFilter"
2
class
="org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter"
/>
这个过滤器使用
装饰模式(Decorate Model),装饰的HttpServletRequest对象。其Wapper是ServletRequest包装类HttpServletRequestWrapper的子类(如SavedRequestAwareWrapper或SecurityContextHolderAwareRequestWrapper),附上获取用户权限信息,request参数,headers 和 cookies 的方法。
rememberMeProcessingFilter
过滤器配置:
<
bean
id
=
"rememberMeProcessingFilter"
class
=
"org.springframework.security.ui.rememberme.RememberMeProcessingFilter"
p:authenticationManager-ref
=
"authenticationManager"
p:rememberMeServices-ref
=
"rememberMeServices"
/>
当SecurityContextHolder中不存在Authentication用户授权信息时,rememberMeProcessingFilter就会调用rememberMeServices 的autoLogin()方法从cookie中获取用户信息自动登录。
anonymousProcessingFilter
过滤器配置:
1
<
bean
id
="anonymousProcessingFilter"
2
class
="org.springframework.security.providers.anonymous.AnonymousProcessingFilter"
3
p:key
="springsecurity"
4
p:userAttribute
="anonymousUser,ROLE_ANONYMOUS"
/>
如果不存在任何授权信息时,自动添加匿名用户身份至SecurityContextHolder中,就是这里配置的userAttribute,系统为用户分配一个ROLE_ANONYMOUS权限。
exceptionTranslationFilter(异常处理过滤器)
,该过滤器用来处理在系统认证授权过程中抛出的异常,主要是
处理
AccessDeniedException
和AuthenticationException两个异常并根据配置跳转到不同URL:
1
<
bean
id
="exceptionTranslationFilter"
2
class
="org.springframework.security.ui.ExceptionTranslationFilter"
3
p:accessDeniedHandler-ref
="accessDeniedHandler"
4 p:authenticationEntryPoint-ref
="authenticationEntryPoint"
/>
5
<!--
处理AccessDeniedException
-->
6
<
bean
id
="accessDeniedHandler"
7 class
="org.springframework.security.ui.AccessDeniedHandlerImpl"
8
p:errorPage
="/accessDenied.jsp"
/>
9
<
bean
id
="authenticationEntryPoint"
10 class
="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint"
11
p:loginFormUrl
="/login.jsp"
12
p:forceHttps
="false"
/>
accessDeniedHandler
用于处理AccessDeniedException异常,当用户没有权限访问当前请求的资源时抛出此异常,并跳转自这里配置的/accessDenied.jsp页面。
authenticationEntryPoint(认证入口点)
,这里定义了用户登录的页面。系统为我们提供了3个认证入口点的实现:
认 证 入 口 点
|
作 用
|
BasicProcessingFilterEntryPoint
|
通过向浏览器发送一个HTTP 401(未授权)消息,由浏览器弹出登录对话框,提示用户登录
|
AuthenticationProcessingFilterEntryPoint
|
将用户重定向到一个基于HTML表单的登录页面
|
CasProcessingFilterEntryPoint
|
将用户重定向至一个Yale CAS登录页面
|
这里我们使用AuthenticationProcessingFilterEntryPoint
认证入口点,提供给用户一个友好的登录界面,只是为了给用户更好的体验。
filterSecurityInterceptor(过滤器安全拦截器)
,该过滤器首先调用认证管理器来判断用户是否已被成功验证,如果没有被验证则重定向到登录界面。否则,从Authentication获取用户的权限信息,然后从objectDefinitionSource中获取URL所对应的权限,最后调用accessDecisionManager(访问决策管理器)来判断用户当前拥有的权限是否与当前受保护的URL资源对应的权限匹配,如果匹配就可以访问该URL资源,
否则将抛出
AccessDeniedException
异常
并
返回客户端浏览器一个403错误(如果用户定义了accessDenied页面则会被重定向到该页,见:异常处理过滤器exceptionTranslationFilter中配置的accessDeniedHandler Bean),访问决策管理的的工作机制将在随后更详细介绍,这里先给出过滤器安全拦截器的配置如下:
1
<
bean
id
="filterSecurityInterceptor"
2
3
class
="org.springframework.security.intercept.web.FilterSecurityInterceptor"
4
5
p:authenticationManager-ref
="authenticationManager"
6
7
p:accessDecisionManager-ref
="accessDecisionManager"
>
8
<
property
name
="objectDefinitionSource"
>
9
<
value
>
<![CDATA[
10
11
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
12
PATTERN_TYPE_APACHE_ANT
13
/admins/**=ROLE_SUPERVISOR
14 /user/**=ROLE_USER,IS_AUTHENTICATED_REMEMBERED
15 /default.jsp=ROLE_USER,IS_AUTHENTICATED_REMEMBERED
16
/**=IS_AUTHENTICATED_ANONYMOUSLY
17
]]>
</
value
>
18
</
property
>
19
</
bean
>
从配置可以看出来,过滤器安全拦截器用到了我们前面配置的认证管理器,过滤器安全拦截器使用authenticationManager并调用它的providers(提供者列表)来对用户的身份进行验证并获取用户拥有的权限。如果用户被成功认证,过滤器安全拦截器将会使用accessDecisionManager(访问决策管理器)来判断已认证的用户是否有权限访问受保护的资源,这些受保护的资源由objectDefinitionSource属性定义。
访问决策管理器(accessDecisionManager):
1
<
bean
id
="accessDecisionManager"
2
class
="org.springframework.security.vote.AffirmativeBased"
3
p:allowIfAllAbstainDecisions
="false"
>
4
<
property
name
="decisionVoters"
>
5
<
list
>
6
<
bean
class
="org.springframework.security.vote.RoleVoter"
/>
7
<
bean
class
="org.springframework.security.vote.AuthenticatedVoter"
/>
8
</
list
>
9
</
property
>
10
</
bean
>
身份验证只是Spring Security安全机制的第一步,访问决策管理器验证用户是否有权限访问相应的资源(filterSecurityInterceptor中objectDefinitionSource属性定义的访问URL需要的属性信息)。
org.springframework.security.
AccessDecisionManager
接口定义了用于验证用户是否有权限访问受保护资源的decide方法,另一个supports方法根据受保护资源的配置属性(即访问这些资源所需的权限)来判断该访问决策管理器是否能做出针对该资源的访问决策。decide方法最终决定用户有无访问权限,
如果没有则抛出
AccessDeniedException
异常(面前也提到过,你应该在回过头去看看)。
与认证管理器类似,访问决策管理器也不是由自己来实现访问控制的,而是通过一组投票者来投票决定(通过调用投票者的vote方法),访问决策管理器统计投票结果并最终完成决策工作。下表列出了系统提供的3个访问决策管理器的实现:
访问
决策管理器
|
如 何 决 策
|
AffirmativeBased
|
当至少有一个投票者投允
许访问
票
时
允
许访问
|
ConsensusBased
|
当所有投票者都投允
许访问
票
时
允
许访问
|
UnanimousBased
|
当没有投票者投拒
绝访问
票
时
允
许访问
|
decisionVoters
属性为
访问决策管理器定义了一组进行投票工作的投票者,那么这些投票者是如何进行投票的呢?这就需要提org.springframework.security.vote.AccessDecisionVoter
接口,所有的投票者都实现了这个接口并实现了其中的vote方法。该接口中还定义了3个int类型的常量:
int ACCESS_GRANTED = 1;
(投赞成票)
int ACCESS_ABSTAIN = 0;
(投弃权票)
int ACCESS_DENIED = -1;
(投反对票)
每个决策投票者都返回这3个常量中一个,这取决与用户是否有权限访问当前请求的资源,访问决策管理器再对这些投票结果进行统计。认证投票者的配置如上面所示。
loggerListener
是一个可选项,它和我们前面配置的Bean或者过滤器没有关系,只是监听系统的一些事件(
实现了ApplicationListener监听接口),被它监听的事件包括AuthenticationCredentialsNotFoundEvent事件,AuthorizationFailureEvent事件,AuthorizedEvent事件,PublicInvocationEvent事件,相信你从他们的名字就能看出来是一些什么样的事件,除非你的e文比我还差劲。loggerListener配置如下:
1
<
bean
id
="loggerListener"
class
="org.springframework.security.event.authentication.LoggerListener"
/>
到此,本例所涉及到的所有配置都介绍完了,在下一篇中会介绍方法安全拦截器,以及如何使用它来保护我们的方法调用,以及前面提到过的会在下一篇中介绍的,这里不在一一列出。
接下来就是JSP页面了,首先是login.jsp:
1
<
c:if
test
="${not empty param.login_error}"
>
2
登录失败,请重试。错误原因:
<
br
/>
3
<
font
color
="red"
>
4
<
c:if
test
="${not empty SPRING_SECURITY_LAST_EXCEPTION}"
>
5
<
c:out
value
="${SPRING_SECURITY_LAST_EXCEPTION}"
></
c:out
>
6
</
c:if
>
7
</
font
>
8
</
c:if
>
9
<
form
action
="<c:url value="
/j_spring_security_check"
/>
" method="post">
10
<
table
>
11
<
tr
>
12
<
td
><
label
for
="username"
>
username:
</
label
></
td
>
13
<
td
><
input
type
="text"
id
="username"
name
="j_username"
value="<c:out value="${SPRING_SECURITY_LAST_USERNAME}"/>"/></td>
14
</
tr
>
15
<
tr
>
16
<
td
><
label
for
="password"
>
password:
</
label
></
td
>
17
<
td
><
input
type
="password"
id
="password"
name
="j_password"
value
=""
/></
td
>
18
</
tr
>
19
<
tr
><
td
></
td
>
20
<
td
><
input
type
="checkbox"
name
="_spring_security_remember_me"
>
两周内记住我
</
td
>
21
</
tr
>
22
<
tr
><
td
colspan
="2"
><
input
type
="submit"
value
="提交"
/>
23
<
input
type
="reset"
value
="重置"
/></
td
></
tr
>
24
</
table
>
25
</
form
>
如果你有看源代码,上面的某些参数,以及本文所有提及的东西你都不应该感到陌生。其它页面也不在列出了,还有就是如何让它运行起来,这些我相信你都能自己搞定。
附件1:springsecurity.rar(不包括JAR包)
补上使用命名空间配置实现的代码,命名空间的详细资料请参考Spring Security中文参考文档,翻译得很好,这里就不在累述了,配置文件中也有比较详细的注释。另外例子中还包括了自定义UserDetailService的实现已经如何Ehcache缓存用户信息,详细的信息将在下一篇中讲述。
附件2:springsecurity-namespace.rar
(包括部分JAR包)