[英]Spring Security - Concurrent request during logout
我們在 Web 應用程序中使用 Spring Security。 大多數頁面都是安全的,即用戶必須登錄才能訪問這些頁面。 它通常工作正常。 但是,我們在注銷期間遇到了不需要的行為。
假設用戶已登錄並向服務器發送請求以加載某些(安全)頁面。 在這個請求完成之前,同一個用戶發送了一個注銷請求(即帶有 servlet_path "/j_spring_security_logout" 的請求)。 注銷請求通常非常快,並且可以比前一個請求更早完成。 當然,注銷請求會清除安全上下文。 因此,前一個請求在其生命周期中丟失了安全上下文,這通常會導致異常。
事實上,用戶不需要“手動”啟動第一個請求。 這種情況可能發生在具有自動刷新的頁面上,即用戶在自動發送刷新后的幾分之一秒內按下注銷鏈接。
從某種角度來看,這可以被認為是一種有意義的行為。 另一方面,我更願意在請求的生命周期中防止這種安全上下文的丟失。
有沒有辦法配置 Spring Security 來避免這種情況? (類似於“當有來自同一會話的其他並發請求時推遲清除安全上下文”或“在單個請求期間僅讀取一次安全上下文並將其緩存以供進一步使用”)
謝謝。
因此,這一切(毫無疑問)是設計使然。 這些春季安全性文檔對發生的事情給出了很好的解釋-引用:
在單個會話中接收並發請求的應用程序中,相同的
SecurityContext
實例將在線程之間共享。 即使正在使用ThreadLocal
,它也是從HttpSession
為每個線程檢索的實例。 如果您希望臨時更改線程在其下運行的上下文,則可能會產生影響。 如果僅使用SecurityContextHolder.getContext()
並在返回的上下文對象上調用setAuthentication(anAuthentication)
,則Authentication
對象將在共享同一SecurityContext
實例的所有並發線程中更改。 您可以自定義SecurityContextPersistenceFilter
的行為以為每個請求創建一個全新的SecurityContext
,從而防止一個線程中的更改影響另一個線程。 或者,您可以在臨時更改上下文的位置創建一個新實例。 方法SecurityContextHolder.createEmptyContext()
始終返回新的上下文實例。
上面引用的一小段內容是:
...您可以自定義
SpringContextPersistenceFilter
的行為...
不幸的是,文檔沒有提供有關如何進行此操作或什至將如何進行處理的任何信息。 這個SO問題問的是問題(本質上是該問題的簡化版本),但是並沒有引起太多關注。
還有一個這樣的SO答案可以為HttpSessionSecurityContextRepository
的內部工作原理提供更多的深度,這可能是需要重寫/更新才能解決此問題的部分。
如果我能在實現中解決這個問題(例如創建上下文的新實例),那么我將更新此答案。
我遇到的問題的根源與從HttpSession
讀取用戶ID屬性有關(在並發的“注銷”請求清除后)。 我決定不創建自己的SpringContextRepository
而是創建一個簡單的過濾器,將當前的Authentication
保存到請求中,然后從那里開始工作。
這是基本的過濾器:
public class SaveAuthToRequestFilter extends OncePerRequestFilter {
public static final String REQUEST_ATTR = SaveAuthToRequestFilter.class.getCanonicalName();
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
throws ServletException, IOException {
final SecurityContext context = SecurityContextHolder.getContext();
if (context != null) {
request.setAttribute(REQUEST_ATTR, context.getAuthentication());
}
filterChain.doFilter(request, response);
}
}
必須通過將以下內容添加到WebSecurityConfigurerAdapter
的configure(final HttpSecurity http)
方法中,將其添加到SecurityContextPersistenceFilter
之后。
http.addFilterAfter(new SaveAuthToRequestFilter(), SecurityContextPersistenceFilter.class)
設置好之后,您可以從HttpServletRequest
讀取“當前”(每個線程/請求) Authentication
( @Autowired
或注入到控制器方法中),然后從那里進行工作。 我認為該解決方案仍然有待改進,但這是我能想到的最輕巧的選擇。 感謝@chimmi和@ sura2k的啟發。
免責聲明:有問題的行為是有目的地實施的。 如果有人決定禁用它,他們應該閱讀SEC-2025 ,它描述了您得到的問題。
如果我正確理解這一點,問題是注銷清除了SecurityContext的authentication
數據,從而使
SecurityContextHolder.getContext().getAuthentication()
返回null。 但是登出並不會清除上下文本身,如果您的第一個請求在登出發生之前設法抓住了它,它將保留在ThreadLocal中以供第一個請求使用。
因此,我們所需要做的就是不清除authentication
,事實證明(因為Spring很棒),對此負責的SecurityContextLogoutHandler
具有一個屬性:
private boolean clearAuthentication = true;
正是我們需要的Javadoc :
如果為true,則從SecurityContext中刪除Authentication以防止並發請求出現問題。
您的要求在JIRA SEC-2025中被報告為錯誤(不是功能請求)。 並且他們已經在Spring 3.2中對其進行了修復,因此您希望此處通過設計避免在其中發生/解決該問題。
默認行為是將SecurityContext
保存在HttpSession
,這是afaik spring security目前提供的唯一實現。
即使SecurityContext
是ThreadLocal
它也共享HttpSession
的相同內容。 因此,當SecurityContext
被清除時,它將從HttpSession
刪除,因此在整個用戶會話中將不可用。
您需要做的是,將SecurityContext
另外存儲在HttpSession
之外的HttpServletRequest
(或綁定到HTTP請求的內容)中,然后從HttpSession
讀取回來,如果找不到,則從HttpServletRequest
讀取它。 確保在HttpServletRequest
保存SecurityContext
的深層副本 。 注銷時, 僅從當前發生的HttpSession中清除SecurityContext
。 在這種情況下,即使用戶注銷了,無論正在運行的線程(綁定到HTTP請求)如何,都可以通過HttpServletRequest
訪問SecurityContext
(如果在HttpSession
找不到它-現在正在發生)。 下一個新的HTTP請求將需要身份驗證,因為新請求在HttpSession
(或HttpServletRequest
)中沒有SecurityContext
。
在每個HttpServletRequest
保留一個SecurityContext
的新副本可能只是為了解決HttpServletRequest
而產生的開銷。
為此,您需要閱讀和理解以下Spring實現。
HttpSessionSecurityContextRepository類
SecurityContextPersistenceFilter
使用SecurityContextRepository
加載和保存SecurityContext
。 HttpSessionSecurityContextRepository
是SecurityContextRepository
的象征。
您可能需要通過提供自己的實現或覆蓋必要的實現來替換上述2個類。 (還有其他一些)
參考:
方法實現。
我認為Spring Security文檔明確提到不處理HttpSession
。 這就是為什么有一個SecurityContext
。
當Spring Security工程師沒有建議自己做某事時,IMO只會堅持使用Spring Security實現。 我在這里建議的內容可能不正確,並確保不存在安全漏洞,並且當您進行一些不建議的更改以僅覆蓋一個極端情況時,它不會破壞其他用例。 如果這發生在我身上,我將永遠不會這樣做,因為這是Spring Security專家通過考慮很多我完全不知道的安全事實而決定采用的設計 。
我們認為我們已經通過提供自定義 HttpSessionSecurityContextRepository 對象解決了這個問題。
Spring 安全配置文件:
<sec:http auto-config="false"
entry-point-ref="authenticationProcessingFilterEntryPoint"
security-context-repository-ref="securityContextRepository">
<bean id="securityContextRepository"
class="tn.view.security.CustomHttpSessionSecurityContextRepository"/>
CustomHttpSessionSecurityContextRepository 的實現:
public class CustomHttpSessionSecurityContextRepository extends HttpSessionSecurityContextRepository
{
@Override
public SecurityContext loadContext(final HttpRequestResponseHolder requestResponseHolder)
{
final SecurityContext context = super.loadContext(requestResponseHolder);
// Return a copy of the security context instead of the original security context object
final SecurityContext contextCopy = new SecurityContextImpl();
contextCopy.setAuthentication(context.getAuthentication());
return contextCopy;
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.