使用Scala进行Spring Security配置

工程 | Luke Taylor | 2011年8月1日 | ...

在我之前的文章Spring Security命名空间背后的故事中,我谈到了Spring Security命名空间如何成功地提供了一种简单的替代方案,替代了普通的Spring bean配置,但是当您想要开始自定义其行为时,仍然存在较高的学习曲线。在XML元素和属性背后,创建并连接了各种过滤器和辅助策略,但是,除非阅读处理XML解析的代码,否则没有简单的方法来确定涉及哪些类或它们如何交互的细节。

一段时间以来,我们一直在尝试使用Spring的@Configuration提出一种基于Java的替代方案,它保留了XML命名空间的简单性,同时也使底层行为更加透明和易于自定义。虽然理论上可行,但似乎没有基于Java的解决方案能够满足我们设定的目标,这主要是由于Spring Security中提供了多种选择。

在这篇文章中,我将概述Scala如何为这个问题提供一个优雅的解决方案,其语法对于已经熟悉XML命名空间的人来说非常易读。代码可在github上找到,它是一个正在进行中的项目,我仍然是Scala新手,因此非常欢迎来自专家们的任何反馈或建议。

此处对Spring Security的引用适用于即将发布的3.1版本。此外,如果您以前没有使用过Spring的基于Java的配置,您可能需要查看Chris Beams的这篇博文

问题

让我们首先简要回顾一下命名空间配置的工作方式,重点关注最复杂的web部分。假设我们的配置包含以下内容:

    <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本身会创建几个标准过滤器(包括SecurityContextPeristenceFilterExceptionTranslationFilterFilterSecurityInterceptor)。intercept-url元素描述了FilterSecurityInterceptor用来决定是否应该授予特定请求访问权限的访问规则。

当我们添加其他XML元素时,其他功能会“混合”到过滤器链中。form-login元素添加一个UsernamePasswordAuthenticationFilterlogout添加一个LogoutFilter。如果您添加了一个remember-me元素,您将获得一个RememberMeAuthenticationFilterRememberMeServices实现,其具体类型取决于使用的附加XML属性。

在Spring Security 3.1中,您可以使用多个http元素来创建多个过滤器链。每个链处理应用程序中的不同路径,例如URL /rest/**下的无状态API以及所有其他请求的状态化web应用程序配置。

因此,命名空间提供了许多不同的可能性。我们如何使用@Configuration模型实现类似的功能,保留XML混合方法的简单性,但将底层实现作为语法的一部分公开?

Scala特性作为配置混合

理想情况下,我们希望能够编写类似以下内容:


@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"))
    }
}

其中FormLoginLogout是我们可以在代码编辑器中检查的类型,以查看它们的具体作用。事实证明,通过使用Scala,我们可以做到这一点。上面的配置片段是100%纯Scala代码,除了少数几个小要求(例如需要一个AuthenticationManager)之外,它可以直接用于现有的Java web应用程序。

我们在这里使用了Scala特性将表单登录和注销行为混合到一个基本的过滤器链类中(参见上面代码片段中突出显示的行)。在Java中,我们仅限于单继承和接口的使用。特性有点像接口,但可以包含方法的实现,甚至可以包含将成为其混合类的附加字段,因此它们可以轻松封装特定功能所需的功能。它们还可以覆盖类的内置行为(或者实际上是其他混合的特性)。特性一开始可能需要一些时间来理解。我建议阅读Programming in Scala中的特性章节作为良好的入门介绍。

此处的FilterChain类类似于XML命名空间中的http元素,它提供了一个基本的配置,特性可以混合到其中。它扩展了一个基类StatelessFilterChain,该基类提供处理无状态请求的基本配置,然后FilterChain使用适合使用HttpSession的状态化请求的bean和过滤器来覆盖和增强它。当然,您可以直接在配置中覆盖或操作任何引用(来自类或混合的特性)。您可以在github上的项目wiki中找到有关这些类如何协同工作的更多详细信息。

Scala方法的一个主要好处是您可以立即找出每个特性的作用。由于Scala具有静态类型,Eclipse和IntelliJ IDEA都允许您直接导航到实现。

Image of IDE highlighting

Logout特性的语法高亮

因此,例如,您可以导航到FormLogin特性,并看到它必须混合到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
  }

  ...
}

您还可以看到它混合了几个额外的特性。LoginPage的代码是:


private[scalasec] trait LoginPage extends StatelessFilterChain {
  val loginPage: String

  override def entryPoint : AuthenticationEntryPoint = {
    new LoginUrlAuthenticationEntryPoint(loginPage)
  }
}

因此,这添加了一个名为loginPage抽象值,并使用它来覆盖在StatelessFilterChain中定义的AuthenticationEntryPointFilterChainAuthenticationManager特性还定义了一个名为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]         ^

因此,除非我们为抽象值loginPageauthenticationManager提供值,否则甚至在我们尝试运行应用程序之前就会收到错误。一个可行的配置将是:


@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实例。在实际应用程序中,您最有可能使用Spring Security的ProviderManager实例,并注入一个AuthenticationProvider列表。

Scala函数作为表达式语言(EL)的替代方案

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以及特性在此问题中的适用性印象深刻,无需特殊的DSL。直接在Scala中编写@Configuration类非常容易,通过一些简单的隐式转换和特性的使用,语法与XML命名空间一样简洁,但没有后者所带来的模糊问题。使用预定义的特性和过滤器链类进行编码时,您距离构成配置的Spring Security对象只有一步之遥,并且可以轻松地修改或替换它们,因此您拥有传统Spring bean配置的所有功能,但没有冗长性。能够直接使用Scala函数作为安全访问规则也是一个非常好的额外优势,可以替代EL。

这只是一个概述,而不是深入的讨论。我鼓励您查看github上的代码并尝试不同的配置。即使配置特性的某些实现细节及其支持类对于初学者来说一开始可能有点棘手,您也不需要了解很多Scala就能使用它们来构建配置。github项目也是一个简单的web应用程序,它使用@ConfigurationScalaSecurityConfiguration.scala。这是一个良好的起点,因为它包含几个示例配置。

IDE对Scala的支持一直在不断改进。STS用户可以从STS扩展选项卡安装Scala支持(我在STS 2.7.1中测试了这一点)。同时,您还可以安装Gradle支持,并将项目导入为Gradle构建。导入后,只需向项目添加Scala特性即可。最新版本的IntelliJ IDEAScala插件也非常易于使用,但您可能需要尝试一个夜间构建版本以获得最新的功能和修复。

获取Spring新闻

通过Spring新闻保持联系

订阅

领先一步

VMware提供培训和认证,以快速提升您的进度。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部