Spring Security 基于bean 的高级配置
正如我们在前面几页中看到的那样,基于 bean 的 Spring Security 配置尽管比较复杂,但是提供了一定层次的灵活性,如果复杂应用需要超过 security XML 命名空间风格配置所允许的功能时会用到。
我们将利用这个章节来阐明可用的一些配置选项以及怎么使用。尽管我们不能提供每个可能属性的细节,但是我们鼓励基于在本章和以前章节学到的内容你开始探索并查询 Javadoc 和 Spring Security 的源码。你仅仅会受限于对这个框架是如何组织起来的知识和你的想象力。
Session 生命周期的调整元素
Spring Security 有很多地方影响用户的 HttpSession 的生命周期。有很多功能只有将相关类配置成 Spring bean 时才可用。以下的表格列出了能够影响 session 创建和销毁的 bean 属性:
Class (类) | 属性 | 默认值 | 描述 |
AbstractAuthentication ProcessingFilter ( UsernamePasswordAuthenticationFilter 的父类) | allowSessionCreation | true | 如果为 true ,当认证失败时创建一个新的 session (存储异常) |
UsernamePasswordAuthenticationFilter | allowSessionCreation | true | 如果为 true 的话,这个特殊的过滤器将会创建一个 session 存储最后尝试的用户名。 |
SecurityContextLogoutHandler | invalidateHttpSession | true | 如果为 true , HttpSession 将会失效(参考 Servlet 规范了解 session 失效的细节) |
SecurityContextPersistenceFilter | forceEagerSessionCreation | false | 如果为 true ,该过滤器将会在执行链中其它过滤器之前创建一个 session 。 |
HttpSessionSecurityContextRepository | allowSessionCreation | true | 如果为 true ,如果在请求结束时 session 中还没有 SecurityContext 的话, SecurityContext 将存储到 session 中。 |
取决于你应用的需要,需要审慎分析用户 session ——包括认证的和非认证的——并调整相应的 session 生命周期。
手动配置其它通用的服务
还有一些其它的服务,我们已经在 security 命名空间自动配置中使用了。让我们将其添加上,这样就会有一个完整的基本配置实现。
声明缺失的过滤器
我们将要增强手动配置的过滤器链,添加三个我们还没有配置的服务。包含处理退出、 remember me 以及异常转换。一旦我们完成这些过滤器,我们将会有完整的功能并可以在它启动时深入了解一些有趣的配置选项。
以下缺失的过滤器将会添加到我们的过滤器链中:
- < bean id = "springSecurityFilterChain"
- class = "org.springframework.security.web.FilterChainProxy" >
- < security:filter-chain-map path-type = "ant" >
- < security:filter-chain pattern = "/**" filters ="
- securityContextPersistenceFilter,
- logoutFilter,
- usernamePasswordAuthenticationFilter,
- rememberMeAuthenticationFilter,
- anonymousAuthenticationFilter,
- exceptionTranslationFilter,
- filterSecurityInterceptor" />
- </ security:filter-chain-map >
- </ bean >
正如在前面做的那样,现在我们需要声明这些 Spring bean 并配置他们以及所有依赖的服务 bean 。
LogoutFilter
LogoutFilter 的基本配置(它默认用来响应虚拟 URL /j_spring_security_logout )如下:
- < bean id = "logoutFilter" class ="org.springframework.security
- .web.authentication.logout.LogoutFilter">
- <!-- the post-logout destination -->
- < constructor-arg value = "/" />
- < constructor-arg >
- < array >
- < ref local = "logoutHandler" />
- </ array >
- </ constructor-arg >
- < property name = "filterProcessesUrl" value = "/logout" />
- </ bean >
你会发现构造 LogoutFilter 的方法与其它的认证过滤器都不一样,使用了构造方法而不是 bean 属性。第一个构造参数是用户退出后要转向的 URL ,第二个参数是对 LogoutHandler 实例的引用。
【依赖注入中的构造器哲学。如果你使用 Spring 有一段时间了,可能已经讨论过以适当的面向对象方式管理需要的依赖。尽管 setter 方式对 Spring 的用户来说很便利,但是追求纯正面向对象的人经常会认为如果一个依赖影响到类的功能,那它应该是构造方法签名的一部分(思考一下在 Spring 以前你是怎样写类的)。鉴于 Spring Security 已经发展了有些年头,不同的作者对此有不同的观点,而我们碰巧正好遇到一个或两个这方面的不一致。两种风格的依赖模型都可以——到底使用哪种风格其实取决于你喜欢哪种类型的需求依赖模型。】
我们还要声明一个简单的 LogoutHandler 的如下:
- < bean id = "logoutHandler" class ="org.springframework.security
- .web.authentication.logout.SecurityContextLogoutHandler"/>
你可能认出这个 LogoutHandler 在我们前几页关于 session 处理的表格中出现过。一个明确配置 logout 处理器的好处就是你能修改用户退出时默认的 session 处理行为。一旦我们完成这两点的配置, Log Out 链接就能够再次开始工作了。
请记住在第三章 security 命名空间配置中,我们曾经修改应用使用一个不那么明显绑定 Spring Security 的退出 URL 。当我们从 security 命名空间配置方式修改为明确 bean 定义时,这是我们在这个 bean 定义时重写 filterProcessesUrl 属性的原因,以保持应用的配置保持持续性。
RememberMeAuthenticationFilter
你可能也会回忆起在第三章中使用的 remember me 功能。即使在基于 bean 的配置中,我们也要保持这个有用的 remember me 功能。这关系到一个新的过滤器、新的支持 bean 以及修改一些其它的 bean 。首先让我们看一下这个过滤器:
- < bean id = "rememberMeAuthenticationFilter"
- class ="org.springframework.security.web
- .authentication.rememberme.RememberMeAuthenticationFilter">
- < property name = "rememberMeServices" ref = "rememberMeServices" />
- < property name = "authenticationManager" ref = "customAuthenticationManager" />
- </ bean >
你会发现对 rememberMeServices 的引用,而它还没有定义。现在让我们定义这个 bean :
- < bean id = "rememberMeServices"
- class ="org.springframework.security.web.authentication
- .rememberme.PersistentTokenBasedRememberMeServices">
- < property name = "key" value = "jbcpPetStore" />
- < property name = "tokenValiditySeconds" value = "3600" />
- < property name = "tokenRepository" ref = "jdbcRememberMeTokenRepository" />
- < property name = "userDetailsService" ref = "jdbcUserService" />
- </ bean >
RememberMeServices 实现的很多配置属性与 security 命名空间风格的配置很相似。这两种方法的最大不同是 RememberMeServices 从应用配置的细节中抽象出来了(译者注:即在 security 方式的配置中,已经进行了一些抽象),但是在基于 bean 的配置中, bean 声明必须非常了解在 remember me 功能中所关联的所有 bean 。如果你感到迷惑,请回顾第三章到第四章以了解 remember me 功能所关联的类和流程的细节。
我们有另一个 bean 应用来织入 remember me 的 token 存储。这个 bean 定义很简单,如下:
- < bean id = "jdbcRememberMeTokenRepository"
- class ="org.springframework.security.web
- .authentication.rememberme.JdbcTokenRepositoryImpl">
- < property name = "dataSource" ref = "dataSource" />
- </ bean >
最后我们需要声明 AuthenticationProvider ,它负责处理 remember me 认证请求。它的声明如下:
- < bean id = "rememberMeAuthenticationProvider"
- class = "org.springframework.security .authentication.RememberMeAuthenticationProvider" >
- < property name = "key" value = "jbcpPetStore" />
- </ bean >
你可能会记得这个 key 属性在 AuthenticationProvider (用来进行 token 校验)和 RememberMeServices (用来进行 token 生成)之间共享。要保证它们被设置成相同的。你可能想通过一个 properties 文件来设置这个属性值,那是要 PropertyPlaceholderConfigurer 工具类(或其它类似的)。
现在我们需要将这个 AuthenticationProvider 织入到 AuthenticationManager 的列表中。
- < bean id = "customAuthenticationManager"
- class = "org.springframework.security .authentication.ProviderManager" >
- < property name = "providers" >
- < list >
- < ref local = "daoAuthenticationProvider" />
- < ref local =”anonymousAuthenticationProvider” />
- < ref local = "rememberMeAuthenticationProvider" />
- </ list >
- </ property >
- </ bean >
RememberMeServices 必须织入到 UsernamePasswordAuthenticationFilter ,这样 RememberMeServices 就能处理明确的登录成功( remember me cookie 被更新)和失败( remember me 被移除)。(译者注:其实查看源码在登录失败时,没有移除 cookie 的操作)
- < bean id = "usernamePasswordAuthenticationFilter"
- class ="org.springframework.security.web
- .authentication.UsernamePasswordAuthenticationFilter">
- < property name = "authenticationManager"
- ref = "customAuthenticationManager" />
- < property name = "rememberMeServices" ref = "rememberMeServices" />
- </ bean >
最后要记住的一件事(在配置 Spring Security bean 声明时,用户经常遗忘) RememberMeServices 也是 LogoutHandler 功能的一部分,它在用户退出时清理用户的 cookie 。
- < bean id = "logoutFilter"
- class = "org.springframework.security.web .authentication.logout.LogoutFilter" >
- < constructor-arg value = "/" />
- < constructor-arg >
- < array >
- < ref local = "logoutHandler" />
- < ref local = "rememberMeServices" />
- </ array >
- </ constructor-arg >
- < property name = "filterProcessesUrl" value = "/logout" />
- </ bean >
当这些配置到位,我们就完成了明确织入 remember me 功能。你可能没有想到过 <remember-me> 声明在幕后做了多少的工作。
ExceptionTranslationFilter
在 Spring Security 标准的过滤器链中最后一个 servlet 过滤器是 ExceptionTranslationFilter 。从本章我们前面的讨论,回忆一下这个过滤器在认证和授权整个流程中的重要性。最后的这个过滤器需要一个过滤器定义和两个支持 bean 。
- < bean id = "exceptionTranslationFilter"
- class = "org.springframework.security.web .access.ExceptionTranslationFilter" >
- < property name = "authenticationEntryPoint" ref = "authenticationEntryPoint" />
- < property name = "accessDeniedHandler" ref = "accessDeniedHandler" />
- </ bean >
支持 bean 的声明如下:
- < bean id = "authenticationEntryPoint"
- class ="org.springframework.security.web
- .authentication.LoginUrlAuthenticationEntryPoint">
- < property name = "loginFormUrl" value = "/login.do" />
- </ bean >
- < bean id = "accessDeniedHandler"
- class = "org.springframework.security.web .access.AccessDeniedHandlerImpl" >
- < property name = "errorPage" value = "/accessDenied.do" />
- </ bean >
你可能会认出 errorPage 指令定义的 Access Denied 页面就是本章前面访问拒绝配置练习中的那个地址。类似的, loginFormUrl 对应于我们在前面看到的 security 命名空间中的 login-page 属性。
明确配置SpEL 表达式和投票器
让我们看一下与 security 命名空间中 use-expressions="true" 属性等同的 Spring bean 配置:
- < bean class ="org.springframework.security
- .web.access.expression.DefaultWebSecurityExpressionHandler"
- id = "expressionHandler" />
接下来,我们需要建立投票器( Voter )指向表达式处理器,如下:
- < bean class ="org.springframework.security.web.access
- .expression.WebExpressionVoter" id = "expressionVoter" >
- < property name = "expressionHandler" ref = "expressionHandler" />
- </ bean >
最后,我们需要使 AccessDecisionManager bean 使用这个投票器。
- < bean class = "org.springframework.security.access.vote.AffirmativeBased"
- id = "affirmativeBased" >
- < property name = "decisionVoters" >
- < list >
- < ref bean = "expressionVoter" />
- </ list >
- </ property >
- </ bean >
在完成这些配置以后,我们回到了使用 use-expressions="true" 默认设置的那个点上(译者注:即与使用命名空间配置 use-expressions="true" 一样的效果)。但是,既然我们使用了明确配置方式,那么可以使用自定义的类替换默认的某个或全部类。我们将会本章后面的部分看一个这样的例子。
基于 bean 配置方法安全
在第五章中,我们使用 security 命名空间 <global-method-security> 声明的方式配置过方法安全。迁移到明确的、基于 bean 的配置时,我们必须配置需要的主要类和支持类以重写 <global-method-security> 声明的功能。
我们在第五章中,已经开发出完整的以 bean 配置的方法安全,支持各种类型的注解。鉴于这个配置很简单直接,基本上没有什么有意思的属性,所以我们将完整的配置放在附录:参考资料 中以及本章的 dogstore-explicit-base.xml 配置文件里面。
包装明确的配置
到此时,我们已经完成了配置基于 bean 的 Spring security ,应用也是功能完整的了。如果你想体验 security 命名空间风格的配置与 bean 声明的配置,很容易修改 web.xml 中对 Spring 配置文件的引用从而把一系列配置文件的集合切换到另一个。
l security 命名空间的配置文件为 web.xml 引用的 dogstore-base.xml 和 dogstore-security.xml ;
l 基于 bean 的配置文件为 web.xml 引用的 dogstore-explicit-base.xml 。
从此往后,本书的练习中将会假设你使用的是 security 命名空间配置。如果特定的功能只能使用基于 bean 的配置,我们将会明确说明。我们也会包含一些基于 bean 配置的细节,因为我们知道一些开发人员更愿意拥有这个级别上对安全框架的控制。
我们应该选择什么类型的配置呢?
希望你的大脑没有从我们刚才的所有配置中变迷糊。你可能会想知道,对于一个典型的工程,应该选择什么类型的配置呢。正如你可能预料的那样,答案是取决于你项目的复杂性以及你重写或自定义 Spring Security 元素的程度。
配置类型 | 好处 |
security 命名空间 | l 强大、简洁语法,适用于通常的 web 和方法安全配置; l 用户配置复杂功能时,并不需要知道组件在幕后是如何交互的; l security 命名空间进行代码检查和并警告多种潜在的配置缺陷; l 明显减少缺失配置步骤的可能性。 |
明确的 bean 定义 | l 允许最大灵活性以扩展、重写以及删节标准的 Spring Security 功能; l 允许自定义过滤器链及根据 URL 模式(使用 <filter-chain> 元素的 pattern 属性)的认证方法。这可能在混合 web service 或 REST 认证以及用户认证时用到; l 不用直接将配置文件绑定到 Spring Security 命名空间处理上; l 认证管理器可以明确配置或重写; l 与更简单的 security 命名空间相比,暴露了有更多的配置选项。 |
对于大多数项目,比较明智的是以 security 命名空间开始,并在可能的情况下继续使用直到你的应用需要它不满足的功能。要记住的是,自己配置所有需要的 bean 和 bean 间的依赖关系将会明显增加复杂度,在开始之前,你应该对 Spring Security 的结构(可能还包括底层代码)有清晰的理解。