前面介绍了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被创建的时候通过调用ApplicationContextpublishEvent(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(异常处理过滤器),该过滤器用来处理在系统认证授权过程中抛出的异常,主要是处理AccessDeniedExceptionAuthenticationException两个异常并根据配置跳转到不同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包)