使用 Scala 配置 Spring Security

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

在之前的一篇文章《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 的這篇部落格文章

問題

讓我們首先簡要回顧一下名稱空間配置如何工作,重點關注 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 會建立幾個標準過濾器(包括 SecurityContextPersistenceFilterExceptionTranslationFilterFilterSecurityInterceptor)。intercept-url 元素描述了 FilterSecurityInterceptor 用來決定是否應授予特定請求訪問許可權的訪問規則。

當我們新增其他 XML 元素時,附加功能會“混合”到過濾器鏈中。form-login 元素新增一個 UsernamePasswordAuthenticationFilter,而 logout 新增一個 LogoutFilter。如果您添加了 remember-me 元素,您將獲得一個 RememberMeAuthenticationFilterRememberMeServices 實現,其具體型別取決於所使用的附加 XML 屬性。

在 Spring Security 3.1 中,您將能夠使用多個 http 元素來建立多個過濾器鏈。每個鏈處理應用程式中不同的路徑,例如 URL /rest/** 下的無狀態 API 和處理所有其他請求的有狀態 Web 應用程式配置。

因此,名稱空間提供了許多不同的可能性。我們如何使用 @Configuration 模型實現類似的功能,既保留 XML 混合方法的簡單性,又將底層實現暴露為語法的一部分?

Scala Trait 作為配置混合

理想情況下,我們希望能夠編寫類似以下的程式碼


@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 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 都允許您直接導航到實現

Image of IDE highlighting

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 中定義的 AuthenticationEntryPointFilterChainAuthenticationManager 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]         ^

因此,除非我們為抽象值 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 例項。在實際應用中,您很可能會使用注入了一系列 AuthenticationProvider 的 Spring Security 的 ProviderManager 例項。

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 給我留下了深刻的印象,trait 的使用非常適合解決這個問題,而且無需特殊的 DSL。直接用 Scala 編寫 @Configuration 類非常簡單,透過一些簡單的隱式轉換和 trait 的使用,語法與 XML 名稱空間一樣簡潔,但沒有後者固有的混淆問題。使用預定義的 trait 和過濾器鏈類進行編碼時,您距離構成配置的 Spring Security 物件只有一步之遙,可以輕鬆修改或替換它們,因此您擁有傳統 Spring Bean 配置的所有強大功能,同時又沒有冗長。能夠直接使用 Scala 函式作為安全訪問規則也是一個很好的額外優勢,可以替代 EL。

這只是一個概述,而不是深入的討論。我鼓勵您檢視 github 上的程式碼並嘗試不同的配置。儘管配置 trait 及其支援類的一些實現細節對於初學者來說可能有點棘手,但您不需要了解太多 Scala 即可使用它們來構建配置。github 專案也是一個使用 @ConfigurationScalaSecurityConfiguration.scala 的簡單 Web 應用。這是一個很好的起點,因為它包含幾個示例配置。

IDE 中對 Scala 的支援一直在改進。STS 使用者可以從 STS 擴充套件選項卡安裝 Scala 支援(我在 STS 2.7.1 中測試過)。在此過程中,您還可以安裝 Gradle 支援並將專案作為 gradle 構建匯入。匯入專案後,只需為專案新增 Scala nature。最新版本的 IntelliJ IDEA Scala 外掛也非常實用,不過您可能想嘗試一下每晚構建版本以獲取最新的功能和修復。

訂閱 Spring 新聞簡報

透過 Spring 新聞簡報保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,為您的進步加速。

瞭解更多

獲取支援

Tanzu Spring 透過一項簡單的訂閱提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群中所有即將舉行的活動。

檢視全部