简体   繁体   English

Spring Security - 注销期间的并发请求

[英]Spring Security - Concurrent request during logout

We are using Spring Security in our web application.我们在 Web 应用程序中使用 Spring Security。 Most of the pages are secured, ie a user must be logged in to access these pages.大多数页面都是安全的,即用户必须登录才能访问这些页面。 It works fine usually.它通常工作正常。 However, we encounter an unwanted behavior during logout.但是,我们在注销期间遇到了不需要的行为。

Suppose that a user is logged in and sends a request to the sever to load some (secured) page.假设用户已登录并向服务器发送请求以加载某些(安全)页面。 Before this request is completed, the same user sends a logout request (ie request with servlet_path "/j_spring_security_logout").在这个请求完成之前,同一个用户发送了一个注销请求(即带有 servlet_path "/j_spring_security_logout" 的请求)。 The logout request is usually very fast and it can be completed earlier than the former request.注销请求通常非常快,并且可以比前一个请求更早完成。 Of course, the logout request clears the security context.当然,注销请求会清除安全上下文。 Hence, the former request loses the security context in the middle of its life and this usually cause an exception.因此,前一个请求在其生命周期中丢失了安全上下文,这通常会导致异常。

In fact, the user need not start the first request "manually".事实上,用户不需要“手动”启动第一个请求。 This scenario can happen on a page with automatic refresh, ie the user presses the logout link just a fraction of second after a refreshing has been sent automatically.这种情况可能发生在具有自动刷新的页面上,即用户在自动发送刷新后的几分之一秒内按下注销链接。

From one point of view, this can be considered to be a meaningful behavior.从某种角度来看,这可以被认为是一种有意义的行为。 On the other hand, I would prefer to prevent such loss of security context in the middle of the life of a request.另一方面,我更愿意在请求的生命周期中防止这种安全上下文的丢失。

Is there a way to configure Spring Security to avoid this?有没有办法配置 Spring Security 来避免这种情况? (something like "postpone clearing of security context when there are other concurrent requests from the same session" or "read the security context just once during a single request and cache it for further use") (类似于“当有来自同一会话的其他并发请求时推迟清除安全上下文”或“在单个请求期间仅读取一次安全上下文并将其缓存以供进一步使用”)

Thanks.谢谢。

So this is all (unsurprisingly) by design. 因此,这一切(毫无疑问)是设计使然。 These spring security docs give a good explanation as to what's happening - quoting: 这些春季安全性文档对发生的事情给出了很好的解释-引用:

In an application which receives concurrent requests in a single session, the same SecurityContext instance will be shared between threads. 在单个会话中接收并发请求的应用程序中,相同的SecurityContext实例将在线程之间共享。 Even though a ThreadLocal is being used, it is the same instance that is retrieved from the HttpSession for each thread. 即使正在使用ThreadLocal ,它也是从HttpSession为每个线程检索的实例。 This has implications if you wish to temporarily change the context under which a thread is running. 如果您希望临时更改线程在其下运行的上下文,则可能会产生影响。 If you just use SecurityContextHolder.getContext() , and call setAuthentication(anAuthentication) on the returned context object, then the Authentication object will change in all concurrent threads which share the same SecurityContext instance. 如果仅使用SecurityContextHolder.getContext()并在返回的上下文对象上调用setAuthentication(anAuthentication) ,则Authentication对象将在共享同一SecurityContext实例的所有并发线程中更改。 You can customize the behaviour of SecurityContextPersistenceFilter to create a completely new SecurityContext for each request, preventing changes in one thread from affecting another. 您可以自定义SecurityContextPersistenceFilter的行为以为每个请求创建一个全新的SecurityContext ,从而防止一个线程中的更改影响另一个线程。 Alternatively you can create a new instance just at the point where you temporarily change the context. 或者,您可以在临时更改上下文的位置创建一个新实例。 The method SecurityContextHolder.createEmptyContext() always returns a new context instance. 方法SecurityContextHolder.createEmptyContext()始终返回新的上下文实例。

A snippet of the above quote says: 上面引用的一小段内容是:

... you can customise the behaviour of SpringContextPersistenceFilter ... ...您可以自定义SpringContextPersistenceFilter的行为...

Unfortunately, the docs do not provide any information as to how to go about doing this, or how it would even be approached. 不幸的是,文档没有提供有关如何进行此操作或什至将如何进行处理的任何信息。 This SO question asks the very thing (essentially, is a distilled version of this question), but it has not received much attention. 这个SO问题问的是问题(本质上是该问题的简化版本),但是并没有引起太多关注。

There's also this SO answer that provides a bit more depth into the inner workings of HttpSessionSecurityContextRepository , which is likely to be the piece that would need to be re-written/updated in order to tackle this issue. 还有一个这样的SO答案可以为HttpSessionSecurityContextRepository的内部工作原理提供更多的深度,这可能是需要重写/更新才能解决此问题的部分。

I'll update this answer if I come by a good way of addressing this (such as creating a new instance of the context) in implementation. 如果我能在实现中解决这个问题(例如创建上下文的新实例),那么我将更新此答案。

Update 更新资料

The root of the problem I'm experiencing was related to reading a user id attribute out of the HttpSession (after it had been cleared by a concurrent "logout" request). 我遇到的问题的根源与从HttpSession读取用户ID属性有关(在并发的“注销”请求清除后)。 Instead of implementing my own SpringContextRepository I decided to create a simple filter that saves the current Authentication to the request, and then work from there. 我决定不创建自己的SpringContextRepository而是创建一个简单的过滤器,将当前的Authentication保存到请求中,然后从那里开始工作。

Here's the basic filter: 这是基本的过滤器:

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);
    }

}

Which has to be added after the SecurityContextPersistenceFilter by adding the following to your WebSecurityConfigurerAdapter 's configure(final HttpSecurity http) method. 必须通过将以下内容添加到WebSecurityConfigurerAdapterconfigure(final HttpSecurity http)方法中,将其添加到SecurityContextPersistenceFilter之后。

http.addFilterAfter(new SaveAuthToRequestFilter(), SecurityContextPersistenceFilter.class)

After you have that in place, you can read the 'current' (per thread/request) Authentication from the HttpServletRequest ( @Autowired or injected into your controller methods) and work from there. 设置好之后,您可以从HttpServletRequest读取“当前”(每个线程/请求) Authentication@Autowired或注入到控制器方法中),然后从那里进行工作。 I think this solution still leaves something to be desired, but it's the most lightweight option I could think of. 我认为该解决方案仍然有待改进,但这是我能想到的最轻巧的选择。 Thanks to @chimmi and @sura2k for the inspiration. 感谢@chimmi和@ sura2k的启发。

Disclaimer: behaviour in question was implemented purposefully. 免责声明:有问题的行为是有目的地实施的。 And if anyone decides to disable it they should read SEC-2025 that describes the problem you get in return. 如果有人决定禁用它,他们应该阅读SEC-2025 ,它描述了您得到的问题。


If I understand this correctly the problem is that logout clears SecurityContext's authentication data thus making 如果我正确理解这一点,问题是注销清除了SecurityContext的authentication数据,从而使

SecurityContextHolder.getContext().getAuthentication()

return null. 返回null。 But logout does not clear context itself, if your first request managed to grab it before logout happened it stays in ThreadLocal for first request. 但是登出并不会清除上下文本身,如果您的第一个请求在登出发生之前设法抓住了它,它将保留在ThreadLocal中以供第一个请求使用。

So all we need is not to clear authentication , and it turns out (because Spring is awesome) that SecurityContextLogoutHandler that is responsible of this, has a property: 因此,我们所需要做的就是不清除authentication ,事实证明(因为Spring很棒),对此负责的SecurityContextLogoutHandler具有一个属性:

private boolean clearAuthentication = true;

that does exactly what we need, Javadoc : 正是我们需要的Javadoc

If true, removes the Authentication from the SecurityContext to prevent issues with concurrent requests. 如果为true,则从SecurityContext中删除Authentication以防止并发请求出现问题。

Your requirement was reported as a bug (not a feature request) in JIRA SEC-2025 . 您的要求在JIRA SEC-2025中被报告为错误(不是功能请求)。 And They have fixed it in Spring 3.2, so which you expect here to happen/solve implicitly prevents by its design. 并且他们已经在Spring 3.2中对其进行了修复,因此您希望此处通过设计避免在其中发生/解决该问题。

Default behavior is to save the SecurityContext in the HttpSession and that is the only implementation afaik spring security provides at the moment. 默认行为是将SecurityContext保存在HttpSession ,这是afaik spring security目前提供的唯一实现。

Even the SecurityContext is ThreadLocal it shares the same which is in the HttpSession . 即使SecurityContextThreadLocal它也共享HttpSession的相同内容。 So when SecurityContext gets cleaned, it will remove from the HttpSession , hence will be unavailable from the entire user session. 因此,当SecurityContext被清除时,它将从HttpSession删除,因此在整个用户会话中将不可用。

What you need to do is, store the SecurityContext additionally in HttpServletRequest (or something bound to the HTTP request) other than the HttpSession and read it back from HttpSession and if not found, read it from HttpServletRequest . 您需要做的是,将SecurityContext另外存储在HttpSession之外的HttpServletRequest (或绑定到HTTP请求的内容)中,然后从HttpSession读取回来,如果找不到,则从HttpServletRequest读取它。 Make sure to save a deep-copy of SecurityContext in the HttpServletRequest . 确保在HttpServletRequest保存SecurityContext深层副本 When you logs out, clean the SecurityContext only from HttpSession which is currently happening. 注销时, 仅从当前发生的HttpSession中清除SecurityContext In this case whatever the running threads (bound to HTTP requests) will have access to the SecurityContext via HttpServletRequest (if it not found in HttpSession - which is happening to you right now) even if the user has logged out. 在这种情况下,即使用户注销了,无论正在运行的线程(绑定到HTTP请求)如何,都可以通过HttpServletRequest访问SecurityContext (如果在HttpSession找不到它-现在正在发生)。 Next new HTTP requests will need an authentication because new requests has no SecurityContext in the HttpSession ( or HttpServletRequest ). 下一个新的HTTP请求将需要身份验证,因为新请求在HttpSession (或HttpServletRequest )中没有SecurityContext

Keep a new copy of SecurityContext in each HttpServletRequest may be an overhead only to address a corner case. 在每个HttpServletRequest保留一个SecurityContext的新副本可能只是为了解决HttpServletRequest而产生的开销。

To do that you need to read and understand following spring implementations. 为此,您需要阅读和理解以下Spring实现。

  1. HttpSessionSecurityContextRepository class HttpSessionSecurityContextRepository

    SecurityContextPersistenceFilter uses SecurityContextRepository to load and save SecurityContext . SecurityContextPersistenceFilter使用SecurityContextRepository加载和保存SecurityContext HttpSessionSecurityContextRepository is an implemntation of SecurityContextRepository . HttpSessionSecurityContextRepositorySecurityContextRepository的象征。

  2. SaveToSessionResponseWrapper class SaveToSessionResponseWrapper

And you might need to replace above 2 classes by providing your own implementations or overriding necessary implementations. 您可能需要通过提供自己的实现或覆盖必要的实现来替换上述2个类。 (And there may some others as well) (还有其他一些)

Refer: 参考:

method implementations. 方法实现。

I think Spring Security docs clearly mentions not to deal with HttpSession . 我认为Spring Security文档明确提到不处理HttpSession That is why there is a SecurityContext . 这就是为什么有一个SecurityContext

IMO just stick to the Spring Security implementations when there are no recommendations from Spring Security engineers to do something on your own. 当Spring Security工程师没有建议自己做某事时,IMO只会坚持使用Spring Security实现。 What I'm sugessting here may not correct, and make sure there will no security holes and it should not break the other use cases when you do some non-recommended changes only to cover a corner case. 我在这里建议的内容可能不正确,并确保不存在安全漏洞,并且当您进行一些不建议的更改以仅覆盖一个极端情况时,它不会破坏其他用例。 I will never do this if this happened to me, because it is the design that Spring Security experts have decided to go with by considering many many many security facts which I have no idea at all. 如果这发生在我身上,我将永远不会这样做,因为这是Spring Security专家通过考虑很多我完全不知道的安全事实而决定采用的设计

We think we have fixed this problem by providing a custom HttpSessionSecurityContextRepository object.我们认为我们已经通过提供自定义 HttpSessionSecurityContextRepository 对象解决了这个问题。

Spring security config file: Spring 安全配置文件:

  <sec:http auto-config="false"
            entry-point-ref="authenticationProcessingFilterEntryPoint"
            security-context-repository-ref="securityContextRepository">

  <bean id="securityContextRepository"
    class="tn.view.security.CustomHttpSessionSecurityContextRepository"/>

Implementation of 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM