[英]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.