Spring Security 常見問題解答

一般問題

本常見問題解答回答以下一般問題

Spring Security 能否處理我所有的應用程式安全需求?

Spring Security 為您的認證和授權需求提供了一個靈活的框架,但是構建安全應用程式還需要考慮許多其他超出其範圍的問題。Web 應用容易受到各種攻擊,您應該熟悉這些攻擊,最好在開發開始之前就瞭解它們,以便從一開始就將它們考慮進設計和編碼中。請訪問 OWASP 網站,瞭解 Web 應用程式開發者面臨的主要問題以及可用於對抗它們的對策。

為何不使用 web.xml 安全?

假設您正在開發一個基於 Spring 的企業應用程式。您通常需要解決四個安全問題:認證、Web 請求安全、服務層安全(實現業務邏輯的方法)和領域物件例項安全(不同的領域物件可以有不同的許可權)。考慮到這些典型需求,我們有以下幾點考慮

  • 認證:Servlet 規範提供了一種認證方法。但是,您需要配置容器來執行認證,這通常需要編輯特定於容器的“realm”設定。這會導致配置不可移植。此外,如果您需要編寫實際的 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 時最常見的問題

當我嘗試登入時,收到錯誤訊息“Bad Credentials”。這是怎麼回事?

這意味著認證失敗。它沒有說明原因,因為不提供可能幫助攻擊者猜測賬戶名或密碼的詳細資訊是一個好的做法。

這也意味著,如果您線上詢問這個問題,除非您提供額外資訊,否則不應期望得到答案。與任何問題一樣,您應該檢查除錯日誌的輸出,並注意任何異常堆疊跟蹤和相關訊息。您應該在偵錯程式中單步執行程式碼,檢視認證在哪裡失敗以及原因。您還應該編寫一個測試用例,在應用程式外部測試您的認證配置。如果您使用雜湊密碼,請確保儲存在資料庫中的值與您應用程式中配置的 PasswordEncoder 生成的值完全相同。

當我嘗試登入時,我的應用程式進入“死迴圈”。這是怎麼回事?

使用者常見的無限迴圈並重定向到登入頁面的問題,通常是由於意外地將登入頁面配置為“受保護”資源所致。請確保您的配置允許匿名訪問登入頁面,可以透過將其排除在安全過濾器鏈之外,或將其標記為需要 ROLE_ANONYMOUS

如果您的 AccessDecisionManager 包含一個 AuthenticatedVoter,您可以使用 IS_AUTHENTICATED_ANONYMOUSLY 屬性。如果您使用標準名稱空間配置設定,此屬性會自動可用。

從 Spring Security 2.0.1 開始,當您使用基於名稱空間的配置時,會在載入應用程式上下文時進行檢查,如果您的登入頁面看起來受到保護,則會記錄警告訊息。

我收到一條異常訊息:“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 請求從不被快取。

我收到一條異常訊息:“An Authentication object was not found in the SecurityContext”。這是怎麼回事?

以下列表顯示了匿名使用者首次嘗試訪問受保護資源時出現的另一條除錯級別訊息。但是,此列表顯示了當您的過濾器鏈配置中沒有 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“繫結”(bind),這是 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 未被標記為 secure。然而,Spring Security 的會話固定保護可能會干擾這一點,因為它會導致向用戶瀏覽器傳送新的會話 ID cookie,通常帶有 secure 標誌。要解決這個問題,您可以停用會話固定保護。然而,在較新的 Servlet 容器中,您還可以配置會話 cookie 從不使用 secure 標誌。

通常來說,在 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 Token 或停用 CSRF 保護(不推薦後者)。

我正在使用 RequestDispatcher 將請求轉發到另一個 URL,但我的安全約束沒有被應用。

預設情況下,過濾器不應用於轉發或包含。如果您確實希望安全過濾器應用於轉發或包含,則必須在 web.xml 檔案中使用 <dispatcher> 元素(它是 <filter-mapping> 元素的子元素)顯式配置它們。

我已將 Spring Security 的 <global-method-security> 元素新增到我的應用程式上下文中,但是,如果我將安全註解新增到我的 Spring MVC 控制器 bean(Struts action 等)中,它們似乎沒有效果。為什麼?

在 Spring Web 應用程式中,為 DispatcherServlet 持有 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 控制器上應用方法安全。

我有一個使用者已經確定認證透過,但是當我嘗試在某些請求期間訪問 SecurityContextHolder 時,Authentication 是 null。為什麼我看不到使用者資訊?

為什麼我看不到使用者資訊?

如果你透過在匹配 URL 模式的 <intercept-url> 元素中使用 filters='none' 屬性將請求排除在安全過濾器鏈之外,那麼 SecurityContextHolder 不會為該請求填充資訊。檢查除錯日誌,看看請求是否通過了過濾器鏈。(你在閱讀除錯日誌,對吧?)

當使用 URL 屬性時,authorize JSP Tag 不會遵守我的方法安全註解。為什麼?

當在 <sec:authorize> 中使用 url 屬性時,方法安全不會隱藏連結,因為我們無法輕易地反向工程哪個 URL 對映到哪個控制器端點。我們受限,因為控制器可能依賴於請求頭、當前使用者和其他細節來確定要呼叫哪個方法。

Spring Security 架構問題

本節回答常見的 Spring Security 架構問題

我如何知道類 X 在哪個包中?

定位類的最佳方法是在你的 IDE 中安裝 Spring Security 原始碼。發行版包含專案劃分的每個模組的原始碼 JAR 包。將它們新增到你的專案源路徑中,然後你可以直接導航到 Spring Security 類(在 Eclipse 中使用 Ctrl-Shift-T)。這也使得除錯更容易,並允許你透過直接檢視程式碼發生異常的地方來排查異常,以瞭解發生了什麼。

名稱空間元素如何對映到傳統的 Bean 配置?

在參考指南的名稱空間附錄中,對名稱空間建立了哪些 Bean 有一個總體概述。在 blog.springsource.com 上還有一篇詳細的部落格文章,名為“Behind the Spring Security Namespace”。如果你想了解全部細節,程式碼位於 Spring Security 3.0 發行版中的 spring-security-config 模組中。你應該首先閱讀標準 Spring Framework 參考文件中關於名稱空間解析的章節。

“ROLE_”是什麼意思,為什麼我的角色名需要它?

Spring Security 具有基於投票者的架構,這意味著訪問決策由一系列 AccessDecisionVoter 例項做出。投票者作用於“配置屬性”,這些屬性是為受保護的資源(例如方法呼叫)指定的。透過這種方法,並非所有屬性都與所有投票者相關,並且投票者需要知道何時應該忽略某個屬性(棄權)以及何時應該根據屬性值投票授予或拒絕訪問。最常見的投票者是 RoleVoter,它預設情況下在找到帶有 ROLE_ 字首的屬性時投票。它將屬性(例如 ROLE_USER)與當前使用者已被分配的許可權名稱進行簡單比較。如果找到匹配項(他們有一個名為 ROLE_USER 的許可權),它就投票授予訪問權。否則,它就投票拒絕訪問。

你可以透過設定 RoleVoterrolePrefix 屬性來更改字首。如果你的應用程式只需要使用角色,並且不需要其他自定義投票者,你可以將字首設定為空字串。在這種情況下,RoleVoter 將所有屬性視為角色。

我如何知道需要向我的應用程式新增哪些依賴才能使用 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 檔案中標記為“optional”的任何依賴,如果你需要它們,則必須新增到你自己的 pom.xml 檔案中。

執行嵌入式 ApacheDS LDAP 伺服器需要哪些依賴?

如果你使用 Maven,你需要在你的 pom.xml 檔案依賴項中新增以下內容

<dependency>
		<groupId>org.apache.directory.server</groupId>
		<artifactId>apacheds-core</artifactId>
		<version>1.5.5</version>
		<scope>runtime</scope>
</dependency>
<dependency>
		<groupId>org.apache.directory.server</groupId>
		<artifactId>apacheds-server-jndi</artifactId>
		<version>1.5.5</version>
		<scope>runtime</scope>
</dependency>

其他所需的 JAR 包應該透過傳遞依賴拉取。

什麼是 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 請求資料)?

你無法做到(除非藉助於執行緒區域性變數等),因為提供給介面的資訊只有使用者名稱。與其實現 UserDetailsService,不如直接實現 AuthenticationProvider 並從提供的 Authentication 令牌中提取資訊。

在標準的 Web 設定中,Authentication 物件的 getDetails() 方法將返回一個 WebAuthenticationDetails 例項。如果你需要額外資訊,可以將自定義的 AuthenticationDetailsSource 注入到你正在使用的認證過濾器中。例如,如果你正在使用名稱空間,例如帶有 <form-login> 元素,那麼你應該移除此元素,並將其替換為指向顯式配置的 UsernamePasswordAuthenticationFilter<custom-filter> 宣告。

如何從 UserDetailsService 中訪問 HttpSession?

你無法做到,因為 UserDetailsService 對 servlet API 一無所知。如果你想儲存自定義使用者資料,你應該定製返回的 UserDetails 物件。然後可以透過執行緒區域性變數 SecurityContextHolder 在任何時候訪問它。呼叫 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 會返回這個自定義物件。

如果你確實需要訪問 session,你必須透過定製 Web 層來實現。

如何在 UserDetailsService 中訪問使用者的密碼?

你無法做到(而且不應該嘗試,即使你找到了方法)。你可能誤解了它的目的。請參閱 FAQ 中早先的 "什麼是 UserDetailsService?"。

如何在應用程式中動態定義受保護的 URL?

人們經常詢問如何將受保護 URL 和安全元資料屬性之間的對映儲存在資料庫而不是應用程式上下文中。

你應該首先問自己是否真的需要這樣做。如果一個應用程式需要安全,它還需要基於明確的策略進行徹底的安全測試。在部署到生產環境之前,可能需要進行審計和驗收測試。具有安全意識的組織應該意識到,他們認真測試過程的好處可能會因允許透過更改配置資料庫中的一兩行來在執行時修改安全設定而瞬間消失。如果你已經考慮了這一點(也許透過在應用程式中使用多層安全),Spring Security 允許你完全定製安全元資料的來源。如果選擇,你可以使其完全動態化。

方法安全和 Web 安全都由 AbstractSecurityInterceptor 的子類保護,它配置有一個 SecurityMetadataSource,從中獲取特定方法或過濾器呼叫的元資料。對於 Web 安全,攔截器類是 FilterSecurityInterceptor,它使用 FilterInvocationSecurityMetadataSource 標記介面。它操作的“受保護物件”型別是 FilterInvocation。預設實現(在名稱空間 <http> 和顯式配置攔截器時都使用)將 URL 模式列表及其對應的“配置屬性”列表(ConfigAttribute 例項)儲存在記憶體對映中。

要從替代來源載入資料,你必須使用顯式宣告的安全過濾器鏈(通常是 Spring Security 的 FilterChainProxy)來定製 FilterSecurityInterceptor Bean。你不能使用名稱空間。然後,你需要實現 FilterInvocationSecurityMetadataSource 以便為你希望的特定 FilterInvocation 載入資料。FilterInvocation 物件包含 HttpServletRequest,因此你可以獲取 URL 或任何其他相關資訊,以便根據返回的屬性列表做出決策。基本輪廓可能看起來像以下示例

  • Java

  • Kotlin

	public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

		public List<ConfigAttribute> getAttributes(Object object) {
			FilterInvocation fi = (FilterInvocation) object;
				String url = fi.getRequestUrl();
				String httpMethod = fi.getRequest().getMethod();
				List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();

				// Lookup your database (or other source) using this information and populate the
				// list of attributes

				return attributes;
		}

		public Collection<ConfigAttribute> getAllConfigAttributes() {
			return null;
		}

		public boolean supports(Class<?> clazz) {
			return FilterInvocation.class.isAssignableFrom(clazz);
		}
	}
class MyFilterSecurityMetadataSource : FilterInvocationSecurityMetadataSource {
    override fun getAttributes(securedObject: Any): List<ConfigAttribute> {
        val fi = securedObject as FilterInvocation
        val url = fi.requestUrl
        val httpMethod = fi.request.method

        // Lookup your database (or other source) using this information and populate the
        // list of attributes
        return ArrayList()
    }

    override fun getAllConfigAttributes(): Collection<ConfigAttribute>? {
        return null
    }

    override fun supports(clazz: Class<*>): Boolean {
        return FilterInvocation::class.java.isAssignableFrom(clazz)
    }
}

有關更多資訊,請檢視 DefaultFilterInvocationSecurityMetadataSource 的程式碼。

如何針對 LDAP 進行認證,但從資料庫載入使用者角色?

LdapAuthenticationProvider Bean(在 Spring Security 中處理正常的 LDAP 認證)配置有兩個獨立的策略介面,一個執行認證,一個載入使用者許可權,分別稱為 LdapAuthenticatorLdapAuthoritiesPopulatorDefaultLdapAuthoritiesPopulator 從 LDAP 目錄載入使用者許可權,並具有各種配置引數,讓你指定如何檢索這些許可權。

要改用 JDBC,你可以根據你的 schema 實現介面,使用適當的 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 的屬性,但在 schema 中沒有支援。除了放棄使用名稱空間,我還能做什麼?

名稱空間功能故意被限制,因此它不包含所有你可以用普通 Bean 完成的操作。如果你想做一些簡單的事情,例如修改 Bean 或注入不同的依賴,可以透過向配置新增 BeanPostProcessor 來實現。你可以在 Spring 參考手冊 中找到更多資訊。為此,你需要了解一些關於建立了哪些 Bean 的資訊,因此你也應該閱讀早先關於 名稱空間如何對映到 Spring Bean 的問題中提到的部落格文章。

通常,你會將所需的功能新增到 BeanPostProcessorpostProcessBeforeInitialization 方法中。假設你想定製 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 上呼叫它。