使用 Scala 的 Spring Security 設定

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

在先前的文章「Spring Security 命名空間幕後」中,我談到 Spring Security 命名空間在提供純 Spring Bean 設定的簡單替代方案方面非常成功,但當您想要開始自訂其行為時,仍然存在陡峭的學習曲線。 在 XML 元素和屬性背後,會建立並連接各種篩選器和輔助策略,但是,除非閱讀處理 XML 剖析的程式碼,否則沒有簡單的方法可以弄清楚涉及哪些類別或它們如何互動的詳細資訊。

在一段時間以來,我們一直嘗試提出基於 Java 的替代解決方案,使用 Spring 的 @Configuration 類別,它保留了 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 的實例(目標 Bean,我們通常在 web.xml 檔案中將其稱為「springSecurityFilterChain」)。

就其本身而言,http 建立了幾個標準篩選器(包括 SecurityContextPeristenceFilterExceptionTranslationFilterFilterSecurityInterceptor)。 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 特徵作為設定 Mixin

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


@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 覆寫並使用適用於狀態請求的 Bean 和篩選器來擴充它,這些狀態請求利用了 HttpSession。 當然,您可以直接在設定中覆寫或操作任何參考(來自類別或混合特徵)。 您可以在 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 實例。 在實際應用程式中,您很可能會使用 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 設定的所有功能,但沒有冗長性。 作為 EL 的替代方案,能夠直接使用 Scala 函數作為安全性存取規則也是一個非常好的額外好處。

這實際上只是一個概述,而不是深入的討論。 我鼓勵您查看 github 上的程式碼並嘗試不同的設定。 即使設定特徵及其支援類別的一些實作細節最初對於初學者來說可能有點棘手,您也不需要了解很多 Scala 即可使用它們來建構設定。 github 專案也是一個簡單的 Web 應用程式,它使用 @Configuration 類別 ScalaSecurityConfiguration.scala。 這是一個很好的起點,因為它包含多個範例設定。

IDE 中對 Scala 的支援一直在改進。 STS 使用者可以從 STS 擴充功能標籤安裝 Scala 支援(我在 STS 2.7.1 中測試了這一點)。 在您進行操作時,您還可以安裝 Gradle 支援並將專案匯入為 Gradle 建置。 匯入專案後,只需將 Scala 特性新增到專案即可。 最新版本的 Intellij IDEA Scala 外掛程式也非常實用,儘管您可能需要嘗試其中一個夜間建置版本以獲得最新的功能和修復。

取得 Spring 電子報

隨時關注 Spring 電子報

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

深入瞭解

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

深入瞭解

即將舉辦的活動

查看 Spring 社群中所有即將舉辦的活動。

查看全部