Spring Security 常見問題
此常見問題包含以下章節
一般問題
此常見問題解答以下一般問題
Spring Security 可以處理我所有的應用程式安全需求嗎?
Spring Security 為您的身分驗證和授權需求提供了一個彈性的框架,但在建構安全應用程式時,還有許多其他考量因素超出其範圍。網路應用程式容易受到各種攻擊,您應該熟悉這些攻擊,最好在開始開發之前就熟悉,以便從一開始就在設計和編碼時將它們納入考量。請查看 OWASP 網站,以取得關於網路應用程式開發人員面臨的主要問題以及您可以使用的對策資訊。
為何不使用 web.xml 安全性?
假設您正在開發一個基於 Spring 的企業應用程式。您通常需要解決四個安全問題:身分驗證、網路請求安全性、服務層安全性(實作業務邏輯的方法)和網域物件實例安全性(不同的網域物件可以有不同的權限)。考慮到這些典型的需求,我們有以下考量
-
身分驗證:Servlet 規範提供了一種身分驗證方法。但是,您需要設定容器來執行身分驗證,這通常需要編輯容器特定的「領域」設定。這使得組態不具備可移植性。此外,如果您需要編寫實際的 Java 類別來實作容器的身分驗證介面,則會變得更加不具可移植性。透過 Spring Security,您可以實現完全的可移植性 — 甚至達到 WAR 層級。此外,Spring Security 提供了經過生產驗證的身分驗證提供者和機制的選擇,這表示您可以在部署時切換身分驗證方法。這對於軟體供應商來說尤其有價值,他們編寫的產品需要在未知的目標環境中運作。
-
網路請求安全性: Servlet 規範提供了一種保護您的請求 URI 的方法。但是,這些 URI 只能以 servlet 規範自身有限的 URI 路徑格式表示。Spring Security 提供了一種更全面的方法。例如,您可以使用 Ant 路徑或正規表示式,您可以考慮 URI 的部分,而不僅僅是請求的頁面(例如,您可以考慮 HTTP GET 參數),並且您可以實作自己的組態資料執行階段來源。這表示您可以在網路應用程式的實際執行期間動態變更您的網路請求安全性。
-
服務層和網域物件安全性: Servlet 規範中缺乏對服務層安全性或網域物件實例安全性的支援,對於多層應用程式來說是一個嚴重的限制。通常,開發人員要么忽略這些需求,要么在他們的 MVC 控制器程式碼(甚至更糟的是,在視圖內部)中實作安全性邏輯。這種方法存在嚴重的缺點
-
關注點分離: 授權是一個跨領域的關注點,應該如此實作。實作授權程式碼的 MVC 控制器或視圖使得測試控制器和授權邏輯變得更加困難,更難以除錯,並且通常會導致程式碼重複。
-
支援豐富型用戶端和網路服務: 如果最終必須支援其他用戶端類型,則嵌入在網路層中的任何授權程式碼都不可重複使用。應該考慮到 Spring remoting exporters 僅匯出服務層 bean(而不是 MVC 控制器)。因此,授權邏輯需要位於服務層中,以支援多種用戶端類型。
-
分層問題: MVC 控制器或視圖是實作關於服務層方法或網域物件實例的授權決策的不正確架構層。雖然主體可以傳遞到服務層以使其能夠做出授權決策,但這樣做會在每個服務層方法上引入一個額外的引數。一種更優雅的方法是使用
ThreadLocal
來保存主體,儘管這可能會增加開發時間,以至於使用專用的安全框架在成本效益基礎上會變得更經濟。 -
授權程式碼品質: 人們經常說網路框架「使做正確的事情更容易,而使做錯誤的事情更難」。安全框架也是如此,因為它們以抽象的方式為廣泛的目的而設計。從頭開始編寫您自己的授權程式碼不會提供框架提供的「設計檢查」,並且內部授權程式碼通常缺乏從廣泛部署、同儕審查和新版本中出現的改進。
-
對於簡單的應用程式,servlet 規範安全性可能就足夠了。儘管在考慮到網路容器可移植性、組態要求、有限的網路請求安全性彈性以及不存在的服務層和網域物件實例安全性的情況下,很明顯開發人員為何經常尋求替代解決方案。
需要哪些 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 時最常遇到的問題
-
身分驗證
-
Session 管理
-
我正在使用 Spring Security 的並行 session 控制來防止使用者同時多次登入。當我在登入後開啟另一個瀏覽器視窗時,它並沒有阻止我再次登入。為何我可以多次登入?
-
我使用 Tomcat(或其他 servlet 容器),並且已為我的登入頁面啟用 HTTPS,之後切換回 HTTP。它無法運作。在身分驗證後,我最終回到登入頁面。
-
我正在嘗試使用並行 session 控制支援,但它不讓我再次登入,即使我確定我已登出並且沒有超過允許的 session 數量。這是怎麼回事?
-
Spring Security 在某處建立了一個 session,即使我已將 create-session 屬性設定為 never,並組態為不建立 session。這是怎麼回事?
-
-
其他
當我嘗試登入時,我收到一則錯誤訊息,指出「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)
}
Session 管理
Session 管理問題是常見的問題來源。如果您正在開發 Java 網路應用程式,您應該了解如何在 servlet 容器和使用者的瀏覽器之間維護 session。您也應該了解安全和非安全 Cookie 之間的差異,以及使用 HTTP 和 HTTPS 以及在兩者之間切換的影響。Spring Security 與維護 session 或提供 session 識別碼無關。這完全由 servlet 容器處理。
我正在使用 Spring Security 的並行 session 控制來防止使用者同時多次登入。當我在登入後開啟另一個瀏覽器視窗時,它並沒有阻止我再次登入。為何我可以多次登入?
瀏覽器通常為每個瀏覽器實例維護單個 session。您不能同時擁有兩個獨立的 session。因此,如果您在另一個視窗或標籤中再次登入,您只是在同一個 session 中重新驗證身分。因此,如果您在另一個視窗或標籤中再次登入,您是在同一個 session 中重新驗證身分。伺服器不知道標籤、視窗或瀏覽器實例的任何資訊。它所看到的只是 HTTP 請求,並且它根據它們包含的 JSESSIONID
Cookie 的值將這些請求與特定 session 關聯起來。當使用者在 session 期間進行身分驗證時,Spring Security 的並行 session 控制會檢查他們擁有的其他已驗證 session 的數量。如果他們已經使用相同的 session 進行身分驗證,則重新驗證身分無效。
當我透過 Spring Security 進行身分驗證時,為何 session ID 會變更?
使用預設組態,Spring Security 會在使用者進行身分驗證時變更 session ID。如果您使用 Servlet 3.1 或更新版本的容器,則 session ID 只會變更。如果您使用較舊的容器,Spring Security 會使現有的 session 失效,建立一個新的 session,並將 session 資料傳輸到新的 session。以這種方式變更 session 識別碼可防止「session-fixation」攻擊。您可以在線上和參考手冊中找到更多關於這方面的資訊。
我使用 Tomcat(或其他 servlet 容器),並且已為我的登入頁面啟用 HTTPS,之後切換回 HTTP。它無法運作。在身分驗證後,我最終回到登入頁面。
它無法運作 - 在身分驗證後,我最終回到登入頁面。
發生這種情況的原因是,在 HTTPS 下建立的 session(其 session Cookie 被標記為「安全」)隨後無法在 HTTP 下使用。瀏覽器不會將 Cookie 發送回伺服器,並且任何 session 狀態(包括安全內容資訊)都會遺失。首先在 HTTP 中啟動 session 應該可以運作,因為 session Cookie 未被標記為安全。但是,Spring Security 的 Session Fixation Protection 會干擾這一點,因為它會導致將新的 session ID Cookie 發送回使用者的瀏覽器,通常帶有安全標誌。為了解決這個問題,您可以停用 session fixation protection。但是,在較新的 Servlet 容器中,您也可以組態 session Cookie 永遠不要使用安全標誌。
通常,在 HTTP 和 HTTPS 之間切換不是一個好主意,因為任何完全使用 HTTP 的應用程式都容易受到中間人攻擊。為了真正安全,使用者應該在 HTTPS 中開始存取您的網站,並繼續使用它直到他們登出。即使從透過 HTTP 存取的頁面點擊 HTTPS 連結也可能存在風險。如果您需要更多說服力,請查看 sslstrip 等工具。 |
我沒有在 HTTP 和 HTTPS 之間切換,但我的 session 仍然遺失。發生了什麼事?
Session 是透過交換 session Cookie 或將 jsessionid
參數新增到 URL 來維護的(如果您使用 JSTL 輸出 URL,或者如果您在 URL 上呼叫 HttpServletResponse.encodeUrl
(例如,在重新導向之前),則會自動發生這種情況)。如果用戶端停用了 Cookie,並且您沒有重寫 URL 以包含 jsessionid
,則 session 會遺失。請注意,基於安全原因,建議使用 Cookie,因為它不會在 URL 中公開 session 資訊。
我正在嘗試使用並行 session 控制支援,但它不讓我再次登入,即使我確定我已登出並且沒有超過允許的 session 數量。這是怎麼回事?
確保您已將監聽器新增到您的 web.xml
檔案。務必確保在 session 被銷毀時通知 Spring Security session 註冊表。如果沒有它,session 資訊將不會從註冊表中移除。以下範例在 web.xml
檔案中新增了一個監聽器
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
Spring Security 在某處建立了一個 session,即使我已將 create-session 屬性設定為 never,並組態為不建立 session。這是怎麼回事?
這通常表示使用者的應用程式在某處建立了一個 session,但他們並未意識到這一點。最常見的罪魁禍首是 JSP。許多人不知道 JSP 預設會建立 session。若要防止 JSP 建立 session,請將 <%@ page session="false" %>
指令新增到頁面頂部。
如果您在找出 session 在何處建立時遇到問題,您可以新增一些除錯程式碼來追蹤位置。一種方法是新增一個 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>
元素明確地組態這些,<dispatcher>
元素是 <filter-mapping>
元素的子元素。
我已將 Spring Security 的 <global-method-security> 元素新增到我的應用程式內容中,但是,如果我將安全註解新增到我的 Spring MVC 控制器 bean(Struts actions 等),它們似乎沒有效果。為什麼?
在 Spring Web 應用程式中,用於 dispatcher servlet 的 Spring MVC bean 應用程式context,通常與主要應用程式 context 分開。它通常在名為 myapp-servlet.xml
的檔案中定義,其中 myapp
是在 web.xml
檔案中分配給 Spring DispatcherServlet
的名稱。一個應用程式可以有多個 DispatcherServlet
實例,每個實例都有自己隔離的應用程式 context。這些「子」context 中的 bean 對於應用程式的其餘部分是不可見的。 「父」應用程式 context 由您在 web.xml
檔案中定義的 ContextLoaderListener
加載,並且對所有子 context 可見。這個父 context 通常是您定義安全性組態的地方,包括 <global-method-security>
元素。因此,應用於這些 web bean 中方法的任何安全性約束都不會強制執行,因為從 DispatcherServlet
context 看不到這些 bean。您需要將 <global-method-security>
宣告移動到 web context,或將您想要保護的 bean 移動到主要應用程式 context 中。
一般來說,我們建議在服務層而不是在個別 web controller 上應用方法安全性。
Spring Security 架構問題
本節解答常見的 Spring Security 架構問題
我如何知道類別 X 在哪個套件中?
定位類別的最佳方法是在您的 IDE 中安裝 Spring Security 原始碼。發行版包含專案劃分成的每個模組的原始碼 jar。將這些添加到您的專案原始碼路徑,然後您可以直接導航到 Spring Security 類別(在 Eclipse 中為 Ctrl-Shift-T
)。這也使 debug 更容易,並讓您可以通過直接查看發生異常的程式碼來排查異常,以查看那裡發生了什麼。
命名空間元素如何映射到傳統的 bean 組態?
參考指南的命名空間附錄中概述了命名空間建立的 bean。還有一篇名為「Behind the Spring Security Namespace」的詳細部落格文章,網址為 blog.springsource.com。如果您想了解完整詳細資訊,則程式碼位於 Spring Security 3.0 發行版中的 spring-security-config
模組中。您應該首先閱讀標準 Spring Framework 參考文檔中關於命名空間解析的章節。
「ROLE_」是什麼意思,為什麼我的角色名稱需要它?
Spring Security 具有基於投票器的架構,這意味著訪問決策是由一系列 AccessDecisionVoter
實例做出的。投票器作用於為受保護資源(例如方法調用)指定的「組態屬性」。使用這種方法,並非所有屬性都可能與所有投票器相關,並且投票器需要知道何時應該忽略屬性(棄權)以及何時應該根據屬性值投票授予或拒絕訪問權限。最常見的投票器是 RoleVoter
,預設情況下,每當它找到帶有 ROLE_
前綴的屬性時,它就會投票。它會將屬性(例如 ROLE_USER
)與當前使用者已被分配的授權名稱進行簡單比較。如果找到匹配項(他們具有名為 ROLE_USER
的授權),它會投票授予訪問權限。否則,它會投票拒絕訪問權限。
您可以通過設定 RoleVoter
的 rolePrefix
屬性來更改前綴。如果您只需要在應用程式中使用角色,並且不需要其他自訂投票器,則可以將前綴設定為空白字串。在這種情況下,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。對於基本應用程式,您可以從 tutorial 範例開始。對於基本應用程式,您可以從 tutorial 範例開始。如果您想將 LDAP 與嵌入式測試伺服器一起使用,請使用 LDAP 範例作為起點。參考手冊還包括 一個附錄,其中列出了每個 Spring Security 模組的一級依賴項,以及關於它們是否為可選以及何時需要的資訊。
如果您使用 Maven 建置專案,將適當的 Spring Security 模組作為依賴項添加到您的 pom.xml
檔案中,會自動拉取框架所需的核心 jar。Spring Security pom.xml
檔案中標記為「optional」的任何 jar,如果您需要它們,都必須添加到您自己的 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
的實例處理。您需要自訂此類別以處理額外的資料欄位。一種選擇是使用您自己的自訂驗證 token 類別(而不是標準的 UsernamePasswordAuthenticationToken
)。另一種選擇是將額外欄位與使用者名稱串聯起來(例如,使用 :
字元作為分隔符),並將它們傳遞到 UsernamePasswordAuthenticationToken
的使用者名稱屬性中。
您還需要自訂實際的驗證過程。例如,如果您使用自訂驗證 token 類別,則必須編寫 AuthenticationProvider
(或擴展標準的 DaoAuthenticationProvider
)來處理它。如果您已串聯這些欄位,則可以實作您自己的 UserDetailsService
來拆分它們,並載入適當的使用者資料以進行驗證。
當僅請求 URL 的片段值不同時(例如 /thing1#thing2 和 /thing1#thing3),我如何應用不同的 intercept-url 約束?
您無法做到這一點,因為片段不會從瀏覽器傳輸到伺服器。從伺服器的角度來看,URL 是相同的。這是 GWT 使用者的常見問題。
我如何在 UserDetailsService 中訪問使用者的 IP 位址(或其他 web 請求資料)?
您無法做到(除非求助於類似於執行緒本地變數的東西),因為提供給介面的唯一資訊是使用者名稱。您應該直接實作 AuthenticationProvider
,而不是實作 UserDetailsService
,並從提供的 Authentication
token 中提取資訊。
在標準 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 和安全性元資料屬性之間的映射儲存在資料庫中,而不是應用程式 context 中。
您應該問自己的第一件事是您是否真的需要這樣做。如果應用程式需要安全,則還需要根據已定義的策略徹底測試安全性。在推出到生產環境之前,可能需要稽核和驗收測試。有安全意識的組織應該意識到,通過允許在運行時通過更改組態資料庫中的一兩行來修改安全性設定,他們勤奮的測試過程的好處可能會立即消失。如果您已考慮到這一點(也許通過在您的應用程式中使用多層安全性),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 驗證)配置了兩個單獨的策略介面,一個執行驗證,另一個載入使用者授權,分別稱為 LdapAuthenticator
和 LdapAuthoritiesPopulator
。DefaultLdapAuthoritiesPopulator
從 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 添加到您的應用程式 context 中,並將其注入到 LdapAuthenticationProvider
中。參考手冊的 LDAP 章節中關於使用顯式 Spring bean 組態 LDAP 的部分涵蓋了這一點。請注意,在這種情況下,您不能使用命名空間進行組態。您還應該查閱 security-api-url[Javadoc],以獲取相關的類別和介面。
我想修改由命名空間建立的 bean 的屬性,但在 schema 中沒有任何內容支援它。在放棄使用命名空間之前,我可以做些什麼?
命名空間功能有意受到限制,因此它並未涵蓋您可以使用普通 bean 完成的所有操作。如果您想做一些簡單的事情,例如修改 bean 或注入不同的依賴項,您可以通過將 BeanPostProcessor
添加到您的組態中來完成。您可以在 Spring Reference Manual 中找到更多資訊。為此,您需要了解一些關於建立哪些 bean 的資訊,因此您還應該閱讀先前關於 命名空間如何映射到 Spring bean 的問題中提到的部落格文章。
通常,您會將您需要的功能添加到 BeanPostProcessor
的 postProcessBeforeInitialization
方法中。假設您想要自訂 UsernamePasswordAuthenticationFilter
(由 form-login
元素建立)使用的 AuthenticationDetailsSource
。您想要從請求中提取名為 CUSTOM_HEADER
的特定 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
}
}
然後,您將在您的應用程式 context 中註冊此 bean。Spring 會自動在應用程式 context 中定義的 bean 上調用它。