Spring Security 常見問題解答
本常見問題解答包含以下部分
一般問題
本常見問題解答回答以下一般問題
Spring Security 能否滿足我的所有應用程式安全要求?
Spring Security 為您的身份驗證和授權要求提供了一個靈活的框架,但是構建安全應用程式還有許多其他考慮因素,這些因素超出了其範圍。Web 應用程式容易受到各種攻擊,您應該熟悉這些攻擊,最好在開始開發之前就熟悉它們,以便從一開始就設計和編碼時考慮到它們。請訪問 OWASP 網站,瞭解 Web 應用程式開發人員面臨的主要問題以及可以用於對抗它們的對策。
為什麼不使用 web.xml 安全?
假設您正在開發一個基於 Spring 的企業應用程式。您通常需要解決四個安全問題:身份驗證、Web 請求安全、服務層安全(實現業務邏輯的方法)和域物件例項安全(不同的域物件可以具有不同的許可權)。考慮到這些典型要求,我們有以下考慮
-
身份驗證:Servlet 規範提供了一種身份驗證方法。但是,您需要配置容器來執行身份驗證,這通常需要編輯特定於容器的“領域”設定。這使得配置不可移植。此外,如果您需要編寫一個實際的 Java 類來實現容器的身份驗證介面,它將變得更加不可移植。使用 Spring Security,您可以實現完全的可移植性——一直到 WAR 級別。此外,Spring Security 提供了多種經過生產驗證的身份驗證提供程式和機制選擇,這意味著您可以在部署時切換身份驗證方法。這對於編寫需要在未知目標環境中工作的產品的軟體供應商尤其有價值。
-
Web 請求安全:Servlet 規範提供了一種保護您的請求 URI 的方法。但是,這些 URI 只能以 servlet 規範自身有限的 URI 路徑格式表達。Spring Security 提供了一種更全面的方法。例如,您可以使用 Ant 路徑或正則表示式,您可以考慮 URI 的其他部分而不僅僅是請求頁面(例如,您可以考慮 HTTP GET 引數),並且您可以實現自己的執行時配置資料來源。這意味著您可以在 Web 應用程式的實際執行過程中動態更改 Web 請求安全。
-
服務層和域物件安全:Servlet 規範中缺少對服務層安全或域物件例項安全的支援,這對於多層應用程式來說是一個嚴重的限制。通常,開發人員要麼忽略這些要求,要麼在他們的 MVC 控制器程式碼中(甚至更糟的是在檢視中)實現安全邏輯。這種方法存在嚴重的缺點
-
關注點分離:授權是一個橫切關注點,應該這樣實現。實現授權程式碼的 MVC 控制器或檢視使得控制器和授權邏輯都更難以測試,更難以除錯,並且通常會導致程式碼重複。
-
支援富客戶端和 Web 服務:如果最終必須支援額外的客戶端型別,則嵌入在 Web 層中的任何授權程式碼都是不可重用的。應該考慮 Spring 遠端匯出程式只匯出服務層 bean(而不是 MVC 控制器)。因此,授權邏輯需要位於服務層以支援多種客戶端型別。
-
分層問題:MVC 控制器或檢視是實現有關服務層方法或域物件例項的授權決策的錯誤架構層。雖然可以將主體傳遞到服務層以使其能夠做出授權決策,但這樣做會在每個服務層方法上引入一個額外的引數。更優雅的方法是使用
ThreadLocal來儲存主體,儘管這可能會增加開發時間,以至於使用專用的安全框架會更經濟(從成本效益的角度來看)。 -
授權程式碼質量:人們常說 Web 框架“讓做正確的事情更容易,讓做錯誤的事情更難”。安全框架也是如此,因為它們以抽象的方式設計用於廣泛的目的。從頭開始編寫自己的授權程式碼不提供框架會提供的“設計檢查”,並且內部授權程式碼通常缺乏廣泛部署、同行評審和新版本所產生的改進。
-
對於簡單的應用程式,servlet 規範安全可能就足夠了。但是,當考慮到 Web 容器可移植性、配置要求、有限的 Web 請求安全靈活性以及不存在的服務層和域物件例項安全時,開發人員經常尋求替代解決方案的原因就變得很清楚了。
需要哪些 Java 和 Spring Framework 版本?
Spring Security 3.0 和 3.1 至少需要 JDK 1.5,並且至少需要 Spring 3.0.3。理想情況下,您應該使用最新的釋出版本以避免問題。
Spring Security 2.0.x 需要最低 JDK 版本 1.4,並基於 Spring 2.0.x 構建。它也應該與使用 Spring 2.5.x 的應用程式相容。
我有一個複雜的場景。可能出了什麼問題?
(此答案透過處理特定場景來解決一般的複雜場景。)
假設您剛接觸 Spring Security,需要構建一個支援透過 HTTPS 進行 CAS 單點登入的應用程式,同時允許某些 URL 進行本地基本身份驗證,並針對多個後端使用者資訊源(LDAP 和 JDBC)進行身份驗證。您已經複製了一些配置檔案,但發現它不起作用。可能出了什麼問題?
在成功構建應用程式之前,您需要了解您打算使用的技術。安全很複雜。使用登入表單和一些硬編碼使用者透過 Spring Security 的名稱空間設定簡單配置相當簡單。遷移到使用後端 JDBC 資料庫也足夠容易。但是,如果您試圖直接跳到像此場景這樣複雜的部署場景,您幾乎肯定會感到沮喪。設定 CAS 等系統、配置 LDAP 伺服器和正確安裝 SSL 證書所需的學習曲線有一個很大的飛躍。因此,您需要一步一步地來。
從 Spring Security 的角度來看,您應該做的第一件事是遵循網站上的“入門”指南。這將引導您完成一系列步驟以啟動並執行,並對框架的運作方式有所瞭解。如果您使用不熟悉的其他技術,您應該進行一些研究並嘗試確保您可以在隔離情況下使用它們,然後再將它們組合到一個複雜的系統中。
常見問題
本節討論人們在使用 Spring Security 時遇到的最常見問題
-
認證
-
會話管理
-
雜項
當我嘗試登入時,出現錯誤訊息“憑據錯誤”。怎麼了?
這意味著身份驗證失敗。它沒有說明原因,因為避擴音供可能幫助攻擊者猜測帳戶名或密碼的詳細資訊是一種好的做法。
這還意味著,如果您線上提問,除非您提供額外資訊,否則不應期望得到答案。與任何問題一樣,您應該檢查除錯日誌的輸出,並注意任何異常堆疊跟蹤和相關訊息。您應該在偵錯程式中逐步執行程式碼,以檢視身份驗證失敗的位置和原因。您還應該編寫一個測試用例,在應用程式外部練習您的身份驗證配置。如果您使用雜湊密碼,請確保資料庫中儲存的值與應用程式中配置的 PasswordEncoder 生成的值完全相同。
當我嘗試登入時,我的應用程式進入“無限迴圈”。怎麼回事?
一個常見的使用者問題是,無限迴圈並重定向到登入頁面是由於不小心將登入頁面配置為“受保護”資源。請確保您的配置允許匿名訪問登入頁面。您可以使用 authorizeHttpRequests DSL 來完成此操作。
|
當您使用基於名稱空間或 DSL 的配置時,會在載入應用程式上下文時進行檢查,如果您的登入頁面似乎受到保護,則會記錄警告訊息。 |
我收到一條異常訊息“Access is denied (user is anonymous);”。怎麼了?
這是一個除錯級別訊息,當匿名使用者首次嘗試訪問受保護資源時發生。
DEBUG [ExceptionTranslationFilter] - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.AccessDeniedException: Access is denied
at org.springframework.security.vote.AffirmativeBased.decide(AffirmativeBased.java:68)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:262)
這是正常的,不必擔心。
為什麼我登出應用程式後仍然可以看到受保護的頁面?
最常見的原因是您的瀏覽器已快取該頁面,並且您正在檢視從瀏覽器快取中檢索的副本。透過檢查瀏覽器是否實際傳送請求來驗證這一點(檢查您的伺服器訪問日誌和除錯日誌,或使用合適的瀏覽器除錯外掛,例如 Firefox 的“Tamper Data”)。這與 Spring Security 無關,您應該配置您的應用程式或伺服器以設定適當的 Cache-Control 響應頭。請注意,SSL 請求從不快取。
我收到一條異常訊息“在 SecurityContext 中未找到 Authentication 物件”。怎麼了?
以下列表顯示了當匿名使用者首次嘗試訪問受保護資源時發生的另一個除錯級別訊息。但是,此列表顯示了當您的過濾器鏈配置中沒有 AnonymousAuthenticationFilter 時發生的情況
DEBUG [ExceptionTranslationFilter] - Authentication exception occurred; redirecting to authentication entry point
org.springframework.security.AuthenticationCredentialsNotFoundException:
An Authentication object was not found in the SecurityContext
at org.springframework.security.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:342)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:254)
這是正常的,不必擔心。
我無法使 LDAP 身份驗證正常工作。我的配置有什麼問題?
請注意,LDAP 目錄的許可權通常不允許您讀取使用者的密碼。因此,通常無法使用 什麼是 UserDetailsService 以及我是否需要它?,其中 Spring Security 將儲存的密碼與使用者提交的密碼進行比較。最常見的方法是使用 LDAP“繫結”,這是 LDAP 協議支援的操作之一。透過這種方法,Spring Security 透過嘗試以使用者身份向目錄進行身份驗證來驗證密碼。
LDAP 身份驗證最常見的問題是對目錄伺服器樹結構和配置缺乏瞭解。這因公司而異,所以您必須自己找出答案。在嚮應用程式新增 Spring Security LDAP 配置之前,您應該使用標準 Java LDAP 程式碼(不涉及 Spring Security)編寫一個簡單的測試,並確保您可以首先使其工作。例如,要驗證使用者,您可以使用以下程式碼
-
Java
-
Kotlin
@Test
public void ldapAuthenticationIsSuccessful() throws Exception {
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=joe,ou=users,dc=mycompany,dc=com");
env.put(Context.PROVIDER_URL, "ldap://mycompany.com:389/dc=mycompany,dc=com");
env.put(Context.SECURITY_CREDENTIALS, "joespassword");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
InitialLdapContext ctx = new InitialLdapContext(env, null);
}
@Test
fun ldapAuthenticationIsSuccessful() {
val env = Hashtable<String, String>()
env[Context.SECURITY_AUTHENTICATION] = "simple"
env[Context.SECURITY_PRINCIPAL] = "cn=joe,ou=users,dc=mycompany,dc=com"
env[Context.PROVIDER_URL] = "ldap://mycompany.com:389/dc=mycompany,dc=com"
env[Context.SECURITY_CREDENTIALS] = "joespassword"
env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory"
val ctx = InitialLdapContext(env, null)
}
會話管理
會話管理問題是常見問題的來源。如果您正在開發 Java Web 應用程式,您應該瞭解 servlet 容器和使用者瀏覽器之間如何維護會話。您還應該瞭解安全和非安全 cookie 之間的區別,以及使用 HTTP 和 HTTPS 並在兩者之間切換的影響。Spring Security 與維護會話或提供會話識別符號無關。這完全由 servlet 容器處理。
我正在使用 Spring Security 的併發會話控制來防止使用者同時多次登入。登入後,當我開啟另一個瀏覽器視窗時,它並沒有阻止我再次登入。為什麼我可以多次登入?
瀏覽器通常每個瀏覽器例項維護一個會話。您不能同時擁有兩個單獨的會話。因此,如果您在另一個視窗或選項卡中再次登入,您只是在同一會話中重新進行身份驗證。因此,如果您在另一個視窗或選項卡中再次登入,您是在同一會話中重新進行身份驗證。伺服器對選項卡、視窗或瀏覽器例項一無所知。它看到的所有都是 HTTP 請求,並根據它們包含的 JSESSIONID cookie 的值將它們繫結到特定會話。當用戶在會話期間進行身份驗證時,Spring Security 的併發會話控制會檢查他們擁有的其他已身份驗證會話的數量。如果他們已經使用同一會話進行了身份驗證,則重新進行身份驗證沒有效果。
為什麼當我透過 Spring Security 進行身份驗證時會話 ID 會更改?
在預設配置下,當用戶進行身份驗證時,Spring Security 會更改會話 ID。如果您使用 Servlet 3.1 或更新的容器,則會話 ID 只是更改。如果您使用較舊的容器,Spring Security 會使現有會話無效,建立新會話,並將會話資料傳輸到新會話。以這種方式更改會話識別符號可防止“會話固定”攻擊。您可以線上和參考手冊中找到有關此內容的更多資訊。
我使用 Tomcat(或其他 servlet 容器)併為我的登入頁面啟用了 HTTPS,之後切換回 HTTP。它不起作用。身份驗證後我回到了登入頁面。
它不起作用 - 身份驗證後我回到了登入頁面。
發生這種情況是因為在 HTTPS 下建立的會話,其會話 cookie 被標記為“secure”,此後不能在 HTTP 下使用。瀏覽器不會將 cookie 發回伺服器,並且任何會話狀態(包括安全上下文資訊)都會丟失。首先在 HTTP 中啟動會話應該可以,因為會話 cookie 未標記為安全。但是,Spring Security 的 會話固定保護 可能會干擾此操作,因為它會導致新的會話 ID cookie 傳送回用戶的瀏覽器,通常帶有安全標誌。要解決此問題,您可以停用會話固定保護。但是,在較新的 Servlet 容器中,您還可以配置會話 cookie 永遠不使用安全標誌。
|
通常,在 HTTP 和 HTTPS 之間切換不是一個好主意,因為任何使用 HTTP 的應用程式都容易受到中間人攻擊。為了真正安全,使用者應該開始在 HTTPS 中訪問您的網站並繼續使用它直到他們登出。即使從透過 HTTP 訪問的頁面單擊 HTTPS 連結也可能存在風險。如果您需要更多證據,請檢視 sslstrip 等工具。 |
我沒有在 HTTP 和 HTTPS 之間切換,但我的會話仍然丟失。發生了什麼?
會話透過交換會話 cookie 或將 jsessionid 引數新增到 URL 來維護(如果您使用 JSTL 輸出 URL 或在 URL 上呼叫 HttpServletResponse.encodeUrl(例如在重定向之前),這會自動發生)。如果客戶端停用了 cookie,並且您沒有重寫 URL 以包含 jsessionid,則會話將丟失。請注意,出於安全原因,首選使用 cookie,因為它不會在 URL 中暴露會話資訊。
我正在嘗試使用併發會話控制支援,但它不允許我重新登入,即使我確定我已經登出並且沒有超過允許的會話。怎麼了?
確保您已將偵聽器新增到您的 web.xml 檔案。確保 Spring Security 會話登錄檔在會話銷燬時收到通知至關重要。否則,會話資訊將不會從登錄檔中刪除。以下示例在 web.xml 檔案中添加了一個偵聽器
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
Spring Security 在某個地方建立了一個會話,即使我已將其配置為不建立會話,方法是將 create-session 屬性設定為 never。怎麼了?
這通常意味著使用者的應用程式在某個地方建立了一個會話,但他們沒有意識到。最常見的罪魁禍首是 JSP。許多人沒有意識到 JSP 預設建立會話。為了防止 JSP 建立會話,請在頁面頂部新增 <%@ page session="false" %> 指令。
如果您在確定會話建立位置時遇到問題,您可以新增一些除錯程式碼來跟蹤位置。一種方法是向您的應用程式新增一個 javax.servlet.http.HttpSessionListener,它在 sessionCreated 方法中呼叫 Thread.dumpStack()。
執行 POST 時我收到 403 Forbidden。怎麼了?
如果 HTTP POST 返回 HTTP 403 Forbidden 錯誤,但 HTTP GET 正常工作,則問題很可能與 CSRF 相關。要麼提供 CSRF 令牌,要麼停用 CSRF 保護(不建議後者)。
我正在使用 RequestDispatcher 將請求轉發到另一個 URL,但我的安全約束未應用。
預設情況下,過濾器不應用於轉發或包含。如果您確實希望將安全過濾器應用於轉發或包含,則必須在您的 web.xml 檔案中使用 <dispatcher> 元素顯式配置它們,該元素是 <filter-mapping> 元素的子元素。
我已將 Spring Security 的 <global-method-security> 元素新增到我的應用程式上下文,但是,如果我將安全註解新增到我的 Spring MVC 控制器 bean (Struts 動作等),它們似乎不起作用。為什麼不?
在 Spring Web 應用程式中,用於 Dispatcher Servlet 的 Spring MVC bean 的應用程式上下文通常與主應用程式上下文分開。它通常在名為 myapp-servlet.xml 的檔案中定義,其中 myapp 是分配給 web.xml 檔案中 Spring DispatcherServlet 的名稱。一個應用程式可以有多個 DispatcherServlet 例項,每個例項都有自己的獨立應用程式上下文。這些“子”上下文中的 bean 對應用程式的其餘部分不可見。您在 web.xml 檔案中定義的 ContextLoaderListener 載入“父”應用程式上下文,並且對所有子上下文都可見。此父上下文通常是您定義安全配置的位置,包括 <global-method-security> 元素。因此,應用於這些 Web bean 中方法的任何安全約束都不會強制執行,因為這些 bean 不能從 DispatcherServlet 上下文看到。您需要將 <global-method-security> 宣告移動到 Web 上下文,或者將您希望受保護的 bean 移動到主應用程式上下文。
通常,我們建議在服務層而不是在各個 Web 控制器上應用方法安全性。
Spring Security 架構問題
本節討論常見的 Spring Security 架構問題
我怎麼知道類 X 在哪個包中?
定位類的最佳方法是在您的 IDE 中安裝 Spring Security 原始碼。發行版包含專案所劃分的每個模組的源 jar。將這些新增到您的專案源路徑,然後您可以直接導航到 Spring Security 類(Eclipse 中的 Ctrl-Shift-T)。這也使除錯更容易,並允許您透過直接檢視異常發生的程式碼來解決異常,以檢視那裡發生了什麼。
名稱空間元素如何對映到傳統的 bean 配置?
參考指南的名稱空間附錄中對名稱空間建立的 bean 有一個總體概述。在 blog.springsource.com 上還有一篇詳細的部落格文章,名為“Spring Security 名稱空間背後”。如果您想了解所有詳細資訊,程式碼位於 Spring Security 3.0 發行版中的 spring-security-config 模組中。您可能應該首先閱讀標準 Spring Framework 參考文件中關於名稱空間解析的章節。
“ROLE_”是什麼意思?
ROLE_ 是識別給定許可權性質的一種方式。以 ROLE_ 為字首的許可權意味著此許可權是一個角色,可能源自 RBAC 授權模型。
具有字首允許與 OAuth 2.0 範圍(使用 SCOPE_)和來自其他來源的許可權進行明確區分。
您可以選擇不為您的許可權新增字首。現代 Spring Security 授權元件允許您提供完整的許可權名稱,使字首變得不必要。例如,authorizeHttpRequests 和 @PreAuthorize 允許您呼叫 hasAuthority 或 hasRole。
我怎麼知道需要嚮應用程式新增哪些依賴項才能使用 Spring Security?
這取決於您使用的功能以及您正在開發的應用程式型別。使用 Spring Security 3.0,專案 jar 被劃分為清晰不同的功能區域,因此根據您的應用程式要求,很容易確定您需要哪些 Spring Security jar。所有應用程式都需要 spring-security-core jar。如果您正在開發 Web 應用程式,則需要 spring-security-web jar。如果您正在使用安全名稱空間配置,則需要 spring-security-config jar。對於 LDAP 支援,您需要 spring-security-ldap jar。依此類推。
對於第三方 jar,情況並非總是那麼明顯。一個好的起點是從預構建的示例應用程式的 WEB-INF/lib 目錄中複製那些 jar。對於基本應用程式,您可以從教程示例開始。對於基本應用程式,您可以從教程示例開始。如果您想將 LDAP 與嵌入式測試伺服器一起使用,請使用 LDAP 示例作為起點。參考手冊還包含 一個附錄,其中列出了每個 Spring Security 模組的第一級依賴項,並提供了一些關於它們是否可選以及何時需要它們的資訊。
如果您使用 Maven 構建專案,將適當的 Spring Security 模組作為依賴項新增到您的 pom.xml 檔案會自動拉入框架所需的核心 jar。Spring Security pom.xml 檔案中標記為“可選”的任何內容,如果您需要它們,則必須新增到您自己的 pom.xml 檔案中。
執行嵌入式 UnboundID LDAP 伺服器需要哪些依賴項?
您需要將以下依賴項新增到您的專案
-
Maven
-
Gradle
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>7.0.1</version>
<scope>runtime</scope>
</dependency>
implementation 'com.unboundid:unboundid-ldapsdk:7.0.1'
執行嵌入式 ApacheDS LDAP 伺服器需要哪些依賴項?
Spring Security 7 移除了對 Apache DS 的支援。請改用 UnboundID。
什麼是 UserDetailsService 以及我是否需要它?
UserDetailsService 是一個 DAO 介面,用於載入特定於使用者帳戶的資料。它除了為框架中的其他元件載入該資料外,沒有其他功能。它不負責驗證使用者。使用使用者名稱和密碼組合驗證使用者最常由 DaoAuthenticationProvider 執行,該提供程式注入了 UserDetailsService 以便它可以載入使用者的密碼(和其他資料),並將其與提交的值進行比較。請注意,如果您使用 LDAP,此方法可能不起作用。
如果您想自定義身份驗證過程,您應該自己實現 AuthenticationProvider。有關整合 Spring Security 身份驗證與 Google App Engine 的示例,請參閱這篇 部落格文章。
常見操作指南
本節回答有關 Spring Security 的常見操作指南
我需要使用除使用者名稱以外的更多資訊登入。如何新增對額外登入欄位(例如公司名稱)的支援?
這個問題反覆出現,因此您可以透過線上搜尋找到更多資訊。
提交的登入資訊由 UsernamePasswordAuthenticationFilter 例項處理。您需要自定義此類以處理額外的欄位。一種選擇是使用您自己的自定義身份驗證令牌類(而不是標準的 UsernamePasswordAuthenticationToken)。另一種選擇是將額外的欄位與使用者名稱連線起來(例如,使用 : 字元作為分隔符),並將它們作為 UsernamePasswordAuthenticationToken 的使用者名稱屬性傳遞。
您還需要自定義實際的身份驗證過程。例如,如果您使用自定義身份驗證令牌類,則必須編寫一個 AuthenticationProvider(或擴充套件標準的 DaoAuthenticationProvider)來處理它。如果您連線了欄位,則可以實現自己的 UserDetailsService 來拆分它們並載入適當的使用者資料進行身份驗證。
如何應用不同的攔截-url 約束,其中只有請求 URL 的片段值不同(例如 /thing1#thing2 和 /thing1#thing3)?
您不能這樣做,因為片段不會從瀏覽器傳輸到伺服器。從伺服器的角度來看,URL 是相同的。這是 GWT 使用者常見的疑問。
如何在 UserDetailsService 中訪問使用者的 IP 地址(或其他 Web 請求資料)?
您不能(除非求助於執行緒本地變數等),因為提供給介面的唯一資訊是使用者名稱。您應該直接實現 AuthenticationProvider 而不是實現 UserDetailsService,並從提供的 Authentication 令牌中提取資訊。
在標準 Web 設定中,Authentication 物件的 getDetails() 方法將返回 WebAuthenticationDetails 例項。如果您需要額外資訊,可以將自定義 AuthenticationDetailsSource 注入您正在使用的身份驗證過濾器。如果您正在使用名稱空間,例如使用 <form-login> 元素,則應刪除此元素並將其替換為指向顯式配置的 UsernamePasswordAuthenticationFilter 的 <custom-filter> 宣告。
如何從 UserDetailsService 訪問 HttpSession?
您不能,因為 UserDetailsService 不瞭解 servlet API。如果您想儲存自定義使用者資料,則應自定義返回的 UserDetails 物件。然後可以透過執行緒本地 SecurityContextHolder 在任何時候訪問此物件。呼叫 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 將返回此自定義物件。
如果您確實需要訪問會話,則必須透過自定義 Web 層來完成。
如何在 UserDetailsService 中訪問使用者的密碼?
您不能(也不應該,即使您找到了這樣做的方法)。您可能誤解了其目的。請參閱本常見問題解答前面部分的“什麼是 UserDetailsService?”。
如何在應用程式中動態定義受保護的 URL?
人們經常詢問如何將受保護 URL 和安全元資料屬性之間的對映儲存在資料庫中,而不是儲存在應用程式上下文中。
您首先應該問自己是否真的需要這樣做。如果應用程式需要安全,它還需要根據定義的策略進行徹底的安全測試。它可能需要在投入生產環境之前進行審計和驗收測試。一個具有安全意識的組織應該意識到,他們勤奮的測試過程所帶來的好處可能會因為允許透過更改配置資料庫中的一行或兩行來在執行時修改安全設定而立即消失。如果您已考慮到這一點(可能透過在應用程式中使用多層安全),Spring Security 允許您完全自定義安全元資料的來源。如果您選擇,您可以使其完全動態。
方法和 Web 安全都由 AuthorizationManager 的實現來保護。對於 Web 安全,您可以提供自己的 AuthorizationManager<RequestAuthorizationContext> 實現,並將其提供給過濾器鏈 DSL,如下所示
-
Java
-
Kotlin
@Component
public class DynamicAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final MyExternalAuthorizationService authz;
// ...
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
// query the external service
}
}
// ...
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(dynamicAuthorizationManager))
@Component
class DynamicAuthorizationManager : AuthorizationManager<RequestAuthorizationContext?> {
private val rules: MyAuthorizationRulesRepository? = null
// ...
override fun authorize(authentication: Supplier<Authentication?>?, context: RequestAuthorizationContext?): AuthorizationResult {
// look up rules from the database
}
}
// ...
http {
authorizeHttpRequests {
authorize(anyRequest, dynamicAuthorizationManager)
}
}
對於方法安全,您可以提供自己的 AuthorizationManager<MethodInvocation> 實現,並將其提供給 Spring AOP,如下所示
-
Java
-
Kotlin
@Component
public class DynamicAuthorizationManager implements AuthorizationManager<MethodInvocation> {
private final MyExternalAuthorizationService authz;
// ...
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
// query the external service
}
}
// ...
@Bean
static Advisor securedAuthorizationAdvisor(DynamicAuthorizationManager dynamicAuthorizationManager) {
return AuthorizationManagerBeforeMethodInterceptor.secured(dynamicAuthorizationManager)
}
@Component
class DynamicAuthorizationManager : AuthorizationManager<MethodInvocation?> {
private val authz: MyExternalAuthorizationService? = null
// ...
override fun authorize(authentication: Supplier<Authentication?>?, invocation: MethodInvocation?): AuthorizationResult {
// query the external service
}
}
companion object {
@Bean
fun securedAuthorizationAdvisor(dynamicAuthorizationManager: DynamicAuthorizationManager): Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(dynamicAuthorizationManager)
}
}
如何針對 LDAP 進行身份驗證,但從資料庫載入使用者角色?
LdapAuthenticationProvider bean(在 Spring Security 中處理正常的 LDAP 身份驗證)配置有兩個獨立的策略介面,一個執行身份驗證,一個載入使用者許可權,分別稱為 LdapAuthenticator 和 LdapAuthoritiesPopulator。DefaultLdapAuthoritiesPopulator 從 LDAP 目錄載入使用者許可權,並具有各種配置引數,允許您指定應如何檢索這些許可權。
要改用 JDBC,您可以自己實現介面,使用適合您的模式的任何 SQL
-
Java
-
Kotlin
public class MyAuthoritiesPopulator implements LdapAuthoritiesPopulator {
@Autowired
JdbcTemplate template;
List<GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
return template.query("select role from roles where username = ?",
new String[] {username},
new RowMapper<GrantedAuthority>() {
/**
* We're assuming here that you're using the standard convention of using the role
* prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter.
*/
@Override
public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
return new SimpleGrantedAuthority("ROLE_" + rs.getString(1));
}
});
}
}
class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
@Autowired
lateinit var template: JdbcTemplate
override fun getGrantedAuthorities(userData: DirContextOperations, username: String): MutableList<GrantedAuthority?> {
return template.query("select role from roles where username = ?",
arrayOf(username)
) { rs, _ ->
/**
* We're assuming here that you're using the standard convention of using the role
* prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter.
*/
SimpleGrantedAuthority("ROLE_" + rs.getString(1))
}
}
}
然後,您將把這種型別的 bean 新增到您的應用程式上下文中,並將其注入到 LdapAuthenticationProvider 中。這在參考手冊的 LDAP 章中關於使用顯式 Spring bean 配置 LDAP 的部分中有所介紹。請注意,在這種情況下,您不能使用名稱空間進行配置。您還應該查閱相關類和介面的 Javadoc。
我想修改由名稱空間建立的 bean 的屬性,但模式中沒有支援。除了放棄名稱空間使用,我還能做什麼?
名稱空間功能有意受到限制,因此它不涵蓋您可以使用普通 bean 完成的所有操作。如果您想做一些簡單的事情,例如修改 bean 或注入不同的依賴項,可以透過在配置中新增 BeanPostProcessor 來完成。您可以在 Spring 參考手冊 中找到更多資訊。為此,您需要了解一些關於建立了哪些 bean 的資訊,因此您還應該閱讀前面關於 名稱空間如何對映到 Spring bean 的問題的部落格文章。
通常,您將所需的功能新增到 BeanPostProcessor 的 postProcessBeforeInitialization 方法中。假設您想自定義 UsernamePasswordAuthenticationFilter(由 form-login 元素建立)使用的 AuthenticationDetailsSource。您想從請求中提取一個名為 CUSTOM_HEADER 的特定標頭,並在驗證使用者時使用它。處理器類將如下所示
-
Java
-
Kotlin
public class CustomBeanPostProcessor implements BeanPostProcessor {
public Object postProcessAfterInitialization(Object bean, String name) {
if (bean instanceof UsernamePasswordAuthenticationFilter) {
System.out.println("********* Post-processing " + name);
((UsernamePasswordAuthenticationFilter)bean).setAuthenticationDetailsSource(
new AuthenticationDetailsSource() {
public Object buildDetails(Object context) {
return ((HttpServletRequest)context).getHeader("CUSTOM_HEADER");
}
});
}
return bean;
}
public Object postProcessBeforeInitialization(Object bean, String name) {
return bean;
}
}
class CustomBeanPostProcessor : BeanPostProcessor {
override fun postProcessAfterInitialization(bean: Any, name: String): Any {
if (bean is UsernamePasswordAuthenticationFilter) {
println("********* Post-processing $name")
bean.setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, Any?> { context -> context.getHeader("CUSTOM_HEADER") })
}
return bean
}
override fun postProcessBeforeInitialization(bean: Any, name: String?): Any {
return bean
}
}
然後您將在應用程式上下文中註冊此 bean。Spring 會自動在應用程式上下文中定義的 bean 上呼叫它。