領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多在之前的文章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 的這篇博文。
<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 本身會建立幾個標準過濾器(包括 SecurityContextPeristenceFilter、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 專案也是一個簡單的 webapp,它使用 @Configuration 類 ScalaSecurityConfiguration.scala。這是一個很好的起點,因為它包含幾個示例配置。
IDE 中對 Scala 的支援一直在不斷改進。STS 使用者可以從 STS 擴充套件選項卡安裝 Scala 支援(我在 STS 2.7.1 中測試過)。同時,您還可以安裝 Gradle 支援並將專案匯入為 Gradle 構建。匯入專案後,只需為專案新增 Scala 特性即可。最新版本的 Intellij IDEA Scala 外掛也非常好用,不過您可能需要嘗試夜間構建版本以獲取最新功能和修復。