领先一步
VMware 提供培训和认证,为您的进步加速。
了解更多在之前的一篇文章《Spring Security 命名空间的幕后》中,我谈到了 Spring Security 命名空间在提供简单替代普通 Spring Bean 配置方面非常成功,但当你想要开始定制其行为时,仍然存在陡峭的学习曲线。在 XML 元素和属性的背后,各种过滤器和辅助策略被创建和连接在一起,但是,除了阅读处理 XML 解析的代码之外,没有简单的方法可以弄清楚涉及哪些类或它们如何交互的细节。
一段时间以来,我们一直在尝试使用Spring 的 @Configuration
类提出一种基于 Java 的替代解决方案,这种方案既保留了 XML 命名空间的简单性,又使底层行为更透明、更容易定制。虽然理论上可行,但由于 Spring Security 中可用的选项范围很广,没有基于 Java 的解决方案似乎能够达到我们设定的目标。
在这篇文章中,我将概述 Scala 如何为这个问题提供一个优雅的解决方案,其语法对于熟悉 XML 命名空间的人来说非常易读。代码可在github上获取。这是一个正在进行的工作,而且我仍然是 Scala 新手,因此非常欢迎各位专家的任何反馈或建议。
这里提到的 Spring Security 参考适用于即将发布的 3.1 版本。另外,如果您之前没有使用过 Spring 的基于 Java 的配置,您可能想查看 Chris Beams 的这篇博客文章。
<http use-expressions="true">
<intercept-url pattern="/secure/extreme/**" access="hasRole('Admin')" />
<intercept-url pattern="/**" access="hasRole('User')" />
<form-login />
<logout />
</http>
http
元素创建一个 SecurityFilterChain
,用于配置 Spring Security 的 FilterChainProxy
实例(我们在 web.xml
文件中通常称之为 "springSecurityFilterChain" 的目标 bean)。
仅靠 http
会创建几个标准过滤器(包括 SecurityContextPersistenceFilter
、ExceptionTranslationFilter
和 FilterSecurityInterceptor
)。intercept-url
元素描述了 FilterSecurityInterceptor
用来决定是否应授予特定请求访问权限的访问规则。
当我们添加其他 XML 元素时,附加功能会“混合”到过滤器链中。form-login
元素添加一个 UsernamePasswordAuthenticationFilter
,而 logout
添加一个 LogoutFilter
。如果您添加了 remember-me
元素,您将获得一个 RememberMeAuthenticationFilter
和 RememberMeServices
实现,其具体类型取决于所使用的附加 XML 属性。
在 Spring Security 3.1 中,您将能够使用多个 http
元素来创建多个过滤器链。每个链处理应用程序中不同的路径,例如 URL /rest/**
下的无状态 API 和处理所有其他请求的有状态 Web 应用程序配置。
因此,命名空间提供了许多不同的可能性。我们如何使用 @Configuration
模型实现类似的功能,既保留 XML 混合方法的简单性,又将底层实现暴露为语法的一部分?
理想情况下,我们希望能够编写类似以下的代码
@Configuration
class SecurityConfiguration {
@Bean
def filterChainProxy = new FilterChainProxy(formLoginFilterChain)
@Bean
def formLoginFilterChain =
new FilterChain with FormLogin with Logout {
interceptUrl("/secure/**", hasRole("Admin"))
interceptUrl("/**", hasRole("User"))
}
}
其中 FormLogin
和 Logout
是我们可以在代码编辑器中检查以确切了解其功能的类型。事实证明,使用 Scala 我们可以做到这一点。上面的配置片段是 100% 纯 Scala 代码,除了少数几个次要要求(例如需要一个 AuthenticationManager
)之外,它可以在现有的 Java Web 应用程序中直接使用。
我们在这里使用了Scala trait,将表单登录和注销行为混合到一个基本的过滤器链类中(参见上面代码片段中突出显示的行)。在 Java 中,我们仅限于单继承和接口的使用。Trait 有点像接口,但可以包含方法的实现甚至附加字段,这些实现和字段将成为它们混合到的类的一部分,因此它们可以轻松地封装特定功能所需的功能。它们还可以覆盖类的内置行为(或者实际上是其他混合进来的 trait)。Trait 起初可能有点难以理解。我建议阅读《Programming in Scala》中的 trait 章节,作为很好的入门。
这里的 FilterChain
类类似于 XML 命名空间中的 http
元素,提供了一个基本配置,可以将 trait 混合到其中。它扩展了一个基类 StatelessFilterChain
,后者提供了处理无状态请求的基本配置,然后 FilterChain
重写并使用适合于利用 HttpSession
的有状态请求的 bean 和过滤器对其进行增强。当然,您可以在配置中直接覆盖或操纵任何引用(来自类或混合进来的 trait)。您可以在github 上的项目 wiki中找到关于这些类如何协同工作的更多详细信息。
Scala 方法的一个主要优势是您可以立即了解每个 trait 的作用。由于 Scala 具有静态类型,Eclipse 和 IntelliJ IDEA 都允许您直接导航到实现
Logout trait 的语法高亮
因此,例如,您可以导航到 FormLogin
trait,并看到它必须混合到 StatelessFilterChain
实例中(“extends” 子句),并且它添加了对 UsernamePasswordAuthenticationFilter
的引用
trait FormLogin extends StatelessFilterChain with LoginPage with FilterChainAuthenticationManager {
lazy val formLoginFilter = {
val filter = new UsernamePasswordAuthenticationFilter
filter.setAuthenticationManager(authenticationManager)
filter.setRememberMeServices(rememberMeServices)
filter
}
...
}
您还可以看到它混合了几个额外的 trait。LoginPage
的代码是
private[scalasec] trait LoginPage extends StatelessFilterChain {
val loginPage: String
override def entryPoint : AuthenticationEntryPoint = {
new LoginUrlAuthenticationEntryPoint(loginPage)
}
}
因此,这添加了一个名为 loginPage
的抽象值,并使用它来覆盖在 StatelessFilterChain
中定义的 AuthenticationEntryPoint
。FilterChainAuthenticationManager
trait 也定义了一个名为 authenticationManager
的抽象值。回顾上面代码高亮的例子,您可能会想知道为什么“FilterChain
”被红色下划线标注。实际上,这段代码本身无法编译。
error] value loginPage in trait LoginPage of type String is not defined
[error] value authenticationManager in trait FilterChainAuthenticationManager of type org.springframework.security.authentication.AuthenticationManager is not defined
[error] new FilterChain with FormLogin with Logout {
[error] ^
因此,除非我们为抽象值 loginPage
和 authenticationManager
提供值,否则甚至在我们尝试运行应用程序之前就会出现错误。一个可工作的配置将是
@Configuration
class SecurityConfiguration {
@Bean
def filterChainProxy = new FilterChainProxy(formLoginFilterChain)
@Bean
def formLoginFilterChain = {
new FilterChain with FormLogin with Logout {
override val loginPage = "/login.jsp"
override val authenticationManager = testAuthenticationManager
interceptUrl("/secure/extreme/**", hasRole("Admin"))
interceptUrl("/**", hasRole("User"))
}
}
@Bean
def testAuthenticationManager = new TestAuthenticationManager()
}
我们使用标准的 @Bean
语法定义了 AuthenticationManager
实例。在实际应用中,您很可能会使用注入了一系列 AuthenticationProvider
的 Spring Security 的 ProviderManager
实例。
Spring Security 3.0 引入了对 EL 表达式进行访问控制的支持。然而,由于 Scala 支持一等函数,为什么在可以直接传递函数的情况下还要使用无类型字符串呢?如果您之前没见过,这需要一点时间来适应。我建议阅读有关 Scala 对偏函数和柯里化的支持,以完全理解其工作原理。
考虑以下这行代码
interceptUrl("/**", hasRole("User"))
interceptUrl
方法的第二个参数是类型为 (Authentication, HttpServletRequest) => Boolean
的函数,这意味着它必须接受一个 Authentication
对象和一个 HttpServletRequest
并返回一个布尔值。当接收到与此规则匹配的请求时,将调用此函数,并传入用户的 Authentication
对象和请求。这与使用 EL 规则完全相同,但功能更强大,并且也是静态类型的。您可以传入任何具有此签名的函数,因此您可以直接在 Scala 中编写所有访问规则,并轻松地对其进行隔离单元测试。示例代码包含一些模拟当前 EL 支持的函数。同样,您可以在 IDE 中直接导航到实现
def permitAll(a: Authentication, r: HttpServletRequest) = true
def denyAll(a: Authentication, r: HttpServletRequest) = false
def hasRole(role: String)(a: Authentication, r: HttpServletRequest) = a.getAuthorities.exists(role == _.getAuthority)
...
注意 hasRole
有两个参数组(这是 Scala 的另一个特性),这使得我们可以将 hasRole("someRole")
作为所需类型的函数传递给 interceptUrl
方法。这只是对可能性的一个非常基本的说明。您可以编写任何您想要的函数,并直接使用它,无需任何额外的配置要求。
总的来说,Scala 给我留下了深刻的印象,trait 的使用非常适合解决这个问题,而且无需特殊的 DSL。直接用 Scala 编写 @Configuration
类非常简单,通过一些简单的隐式转换和 trait 的使用,语法与 XML 命名空间一样简洁,但没有后者固有的混淆问题。使用预定义的 trait 和过滤器链类进行编码时,您距离构成配置的 Spring Security 对象只有一步之遥,可以轻松修改或替换它们,因此您拥有传统 Spring Bean 配置的所有强大功能,同时又没有冗长。能够直接使用 Scala 函数作为安全访问规则也是一个很好的额外优势,可以替代 EL。
这只是一个概述,而不是深入的讨论。我鼓励您查看 github 上的代码并尝试不同的配置。尽管配置 trait 及其支持类的一些实现细节对于初学者来说可能有点棘手,但您不需要了解太多 Scala 即可使用它们来构建配置。github 项目也是一个使用 @Configuration
类 ScalaSecurityConfiguration.scala 的简单 Web 应用。这是一个很好的起点,因为它包含几个示例配置。
IDE 中对 Scala 的支持一直在改进。STS 用户可以从 STS 扩展选项卡安装 Scala 支持(我在 STS 2.7.1 中测试过)。在此过程中,您还可以安装 Gradle 支持并将项目作为 gradle 构建导入。导入项目后,只需为项目添加 Scala nature。最新版本的 IntelliJ IDEA Scala 插件也非常实用,不过您可能想尝试一下每晚构建版本以获取最新的功能和修复。