【问题标题】:Spring Security - Concurrent request during logoutSpring Security - 注销期间的并发请求
【发布时间】:2016-10-02 09:33:35
【问题描述】:

我们在 Web 应用程序中使用 Spring Security。大多数页面都是安全的,即用户必须登录才能访问这些页面。它通常工作正常。但是,我们在注销期间遇到了不受欢迎的行为。

假设用户已登录并向服务器发送请求以加载某些(安全)页面。在此请求完成之前,同一用户会发送注销请求(即带有 servlet_path "/j_spring_security_logout" 的请求)。注销请求通常非常快,并且可以比前一个请求更早完成。当然,注销请求会清除安全上下文。因此,前一个请求在其生命周期中丢失了安全上下文,这通常会导致异常。

事实上,用户不需要“手动”启动第一个请求。这种情况可能发生在具有自动刷新的页面上,即用户在自动发送刷新后的几分之一秒内按下注销链接。

从某种角度来看,这可以被认为是一种有意义的行为。另一方面,我更愿意在请求的生命周期中防止这种安全上下文的丢失。

有没有办法配置 Spring Security 来避免这种情况? (类似于“当有来自同一会话的其他并发请求时推迟清除安全上下文”或“在单个请求期间仅读取一次安全上下文并将其缓存以供进一步使用”)

谢谢。

【问题讨论】:

  • 我很确定我在我的应用程序中也看到了相同的行为。不幸的是,到目前为止我尝试的所有方法都没有解决它(@AuthenticationPrincipal 方法参数,Principal 方法参数,SecurityContextHolder.getContext().getAuthentication()null 一旦并发注销完成)
  • 在正常情况下点击注销按钮会重定向到您的注销页面吗?

标签: java spring-security concurrency logout


【解决方案1】:

所以这一切(不出所料)都是设计使然。这些spring security docs 很好地解释了正在发生的事情 - 引用:

在单个会话中接收并发请求的应用程序中,相同的SecurityContext 实例将在线程之间共享。即使使用了ThreadLocal,它也是从每个线程的HttpSession 检索到的同一个实例。如果您希望临时更改线程正在运行的上下文,这会产生影响。如果您只使用SecurityContextHolder.getContext(),并在返回的上下文对象上调用setAuthentication(anAuthentication),那么Authentication 对象将在共享相同SecurityContext 实例的所有并发线程中发生变化。您可以自定义SecurityContextPersistenceFilter 的行为,为每个请求创建一个全新的SecurityContext,防止一个线程中的更改影响另一个线程。或者,您可以在临时更改上下文的位置创建一个新实例。 SecurityContextHolder.createEmptyContext() 方法总是返回一个新的上下文实例。

上面引用的一个 sn-p 说:

...您可以自定义SpringContextPersistenceFilter的行为...

不幸的是,文档没有提供有关如何进行此操作或如何处理的任何信息。这个SO question 提出了这个问题(本质上是这个问题的提炼版本),但它没有受到太多关注。

还有这个SO answer 更深入地了解了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);
    }

}

必须在SecurityContextPersistenceFilter 之后添加,方法是将以下内容添加到您的WebSecurityConfigurerAdapterconfigure(final HttpSecurity http) 方法中。

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

完成之后,您可以从HttpServletRequest@Autowired 或注入到您的控制器方法中)读取“当前”(每个线程/请求)Authentication 并从那里开始工作。我认为这个解决方案还有一些不足之处,但它是我能想到的最轻量级的选择。感谢@chimmi 和@sura2k 的启发。

【讨论】:

    【解决方案2】:

    您的要求在 JIRA SEC-2025 中报告为错误(不是功能请求)。他们已经在 Spring 3.2 中修复了它,所以你期望在这里发生/解决它的设计隐含地阻止了它。

    默认行为是将SecurityContext 保存在HttpSession 中,这是目前 afaik spring security 提供的唯一实现。

    即使SecurityContextThreadLocal,它也与HttpSession 中的相同。因此,当SecurityContext 被清理时,它将从HttpSession 中删除,因此将在整个用户会话中不可用。

    您需要做的是,将SecurityContext另外存储在HttpServletRequest(或绑定到HTTP请求的东西)中,而不是HttpSession,然后从HttpSession读回来,如果找不到,请阅读来自HttpServletRequest。确保在HttpServletRequest 中保存SecurityContext深拷贝。当您注销时,从当前正在发生的 HttpSession 中清除 SecurityContext only。在这种情况下,无论正在运行的线程(绑定到 HTTP 请求)都可以通过HttpServletRequest 访问SecurityContext(如果在HttpSession 中没有找到它——这正在发生在你身上),即使用户已经注销.下一个新的 HTTP 请求将需要身份验证,因为新请求在 HttpSession(或 HttpServletRequest)中没有 SecurityContext

    在每个HttpServletRequest 中保留SecurityContext 的新副本可能只是解决极端情况的开销。

    为此,您需要阅读并理解以下 Spring 实现。

    1. HttpSessionSecurityContextRepository

      SecurityContextPersistenceFilter 使用SecurityContextRepository 加载和保存SecurityContextHttpSessionSecurityContextRepositorySecurityContextRepository 的实现。

    2. SaveToSessionResponseWrapper

    您可能需要通过提供自己的实现或覆盖必要的实现来替换上述 2 个类。 (可能还有其他一些)

    参考:

    方法实现。

    我认为 Spring Security 文档明确提到不处理 HttpSession。这就是为什么会有SecurityContext

    当 Spring Security 工程师没有建议您自己做某事时,IMO 只会坚持 Spring Security 实现。我在这里建议的内容可能不正确,并确保没有安全漏洞,并且当您进行一些非推荐的更改只是为了覆盖角落案例时,它不应该破坏其他用例。 如果这发生在我身上,我永远不会这样做,因为这是 Spring Security 专家通过考虑许多我根本不知道的安全事实而决定采用的 设计

    【讨论】:

    • SEC-2025 没有直接映射到这个问题,因为该票证指的是意外登录用户重新登录;而在这里我们面临的问题是一个线程发现自己“注销”,即使它开始为“登录”。话虽如此,您的回答似乎是解决此问题的好方法。
    • @jlb 你应该明白,有了这个你基本上会得到一个只有你知道存在的只读副本,这意味着 Spring Secuity 代码都不会使用它。如果这是您想要的,那么整个问题就没有实际意义了,您可以制作一个简单的过滤器,将上下文存储在本地请求/线程中并获得相同的结果。
    • @chimmi 你是绝对正确的——我在接受赏金后不久就意识到了这一点。我有一个想法,我很快就会更新我的答案 - 我相信这是一种更清洁的方法,尽管它迎合了我的问题。
    • @chimmi 我已经用一个简单的解决方案更新了我的答案。它特别适合我,但我认为它适用于大多数情况。
    【解决方案3】:

    免责声明:有问题的行为是有目的地实施的。如果有人决定禁用它,他们应该阅读SEC-2025,它描述了你得到的问题。


    如果我理解正确,问题是注销会清除 SecurityContext 的authentication 数据,从而使

    SecurityContextHolder.getContext().getAuthentication()
    

    返回空值。但是注销本身并不会清除上下文,如果您的第一个请求在注销发生之前设法获取它,它会在第一个请求时保留在 ThreadLocal 中。

    所以我们只需要不清除authentication,事实证明(因为Spring很棒)负责这个的SecurityContextLogoutHandler有一个属性:

    private boolean clearAuthentication = true;
    

    这正是我们需要的,Javadoc:

    如果为 true,则从 SecurityContext 中删除身份验证以防止并发请求出现问题。

    【讨论】:

    猜你喜欢
    • 2016-02-04
    • 2020-04-02
    • 2016-02-04
    • 2017-04-22
    • 2012-10-09
    • 2018-09-02
    • 2011-06-28
    • 2016-07-12
    • 2011-07-19
    相关资源
    最近更新 更多