AnonymousAuthenticationFilter Avoids Eager SecurityContext Access

Previously AnonymousAuthenticationFilter accessed the SecurityContext to
determine if anonymous authentication needed setup eagerly. Now this is done
lazily to avoid unnecessary access to the SecurityContext which in turn avoids
unnecessary HTTP Session access.

Closes gh-11457
This commit is contained in:
Rob Winch 2022-07-05 14:59:42 -05:00
parent d96b4a0463
commit 0bf985ed7c
2 changed files with 49 additions and 16 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.security.web.authentication;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@ -91,18 +92,33 @@ public class AnonymousAuthenticationFilter extends GenericFilterBean implements
@Override @Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
if (this.securityContextHolderStrategy.getContext().getAuthentication() == null) { Supplier<SecurityContext> deferredContext = this.securityContextHolderStrategy.getDeferredContext();
Authentication authentication = createAuthentication((HttpServletRequest) req); this.securityContextHolderStrategy
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); .setDeferredContext(defaultWithAnonymous((HttpServletRequest) req, deferredContext));
context.setAuthentication(authentication); chain.doFilter(req, res);
this.securityContextHolderStrategy.setContext(context); }
private Supplier<SecurityContext> defaultWithAnonymous(HttpServletRequest request,
Supplier<SecurityContext> currentDeferredContext) {
return () -> {
SecurityContext currentContext = currentDeferredContext.get();
return defaultWithAnonymous(request, currentContext);
};
}
private SecurityContext defaultWithAnonymous(HttpServletRequest request, SecurityContext currentContext) {
Authentication currentAuthentication = currentContext.getAuthentication();
if (currentAuthentication == null) {
Authentication anonymous = createAuthentication(request);
if (this.logger.isTraceEnabled()) { if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> "Set SecurityContextHolder to " this.logger.trace(LogMessage.of(() -> "Set SecurityContextHolder to " + anonymous));
+ this.securityContextHolderStrategy.getContext().getAuthentication()));
} }
else { else {
this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext"); this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
} }
SecurityContext anonymousContext = this.securityContextHolderStrategy.createEmptyContext();
anonymousContext.setAuthentication(anonymous);
return anonymousContext;
} }
else { else {
if (this.logger.isTraceEnabled()) { if (this.logger.isTraceEnabled()) {
@ -110,7 +126,7 @@ public class AnonymousAuthenticationFilter extends GenericFilterBean implements
+ this.securityContextHolderStrategy.getContext().getAuthentication())); + this.securityContextHolderStrategy.getContext().getAuthentication()));
} }
} }
chain.doFilter(req, res); return currentContext;
} }
protected Authentication createAuthentication(HttpServletRequest request) { protected Authentication createAuthentication(HttpServletRequest request) {

View File

@ -17,6 +17,7 @@
package org.springframework.security.web.authentication; package org.springframework.security.web.authentication;
import java.io.IOException; import java.io.IOException;
import java.util.function.Supplier;
import jakarta.servlet.Filter; import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
@ -33,6 +34,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.core.context.SecurityContextImpl;
@ -40,7 +42,6 @@ import org.springframework.security.core.context.SecurityContextImpl;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ -79,19 +80,16 @@ public class AnonymousAuthenticationFilterTests {
public void testOperationWhenAuthenticationExistsInContextHolder() throws Exception { public void testOperationWhenAuthenticationExistsInContextHolder() throws Exception {
// Put an Authentication object into the SecurityContextHolder // Put an Authentication object into the SecurityContextHolder
Authentication originalAuth = new TestingAuthenticationToken("user", "password", "ROLE_A"); Authentication originalAuth = new TestingAuthenticationToken("user", "password", "ROLE_A");
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); SecurityContext originalContext = new SecurityContextImpl(originalAuth);
given(strategy.getContext()).willReturn(new SecurityContextImpl(originalAuth)); SecurityContextHolder.setContext(originalContext);
AnonymousAuthenticationFilter filter = new AnonymousAuthenticationFilter("qwerty", "anonymousUsername", AnonymousAuthenticationFilter filter = new AnonymousAuthenticationFilter("qwerty", "anonymousUsername",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
filter.setSecurityContextHolderStrategy(strategy);
// Test
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("x"); request.setRequestURI("x");
executeFilterInContainerSimulator(mock(FilterConfig.class), filter, request, new MockHttpServletResponse(), executeFilterInContainerSimulator(mock(FilterConfig.class), filter, request, new MockHttpServletResponse(),
new MockFilterChain(true)); new MockFilterChain(true));
// Ensure filter didn't change our original object // Ensure getDeferredContext still
verify(strategy).getContext(); assertThat(SecurityContextHolder.getContext()).isEqualTo(originalContext);
verify(strategy, never()).setContext(any());
} }
@Test @Test
@ -110,6 +108,25 @@ public class AnonymousAuthenticationFilterTests {
// again // again
} }
@Test
public void doFilterDoesNotGetContext() throws Exception {
Supplier<SecurityContext> originalSupplier = mock(Supplier.class);
Authentication originalAuth = new TestingAuthenticationToken("user", "password", "ROLE_A");
SecurityContext originalContext = new SecurityContextImpl(originalAuth);
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
given(strategy.getDeferredContext()).willReturn(originalSupplier);
given(strategy.getContext()).willReturn(originalContext);
AnonymousAuthenticationFilter filter = new AnonymousAuthenticationFilter("qwerty", "anonymousUsername",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
filter.setSecurityContextHolderStrategy(strategy);
filter.afterPropertiesSet();
executeFilterInContainerSimulator(mock(FilterConfig.class), filter, new MockHttpServletRequest(),
new MockHttpServletResponse(), new MockFilterChain(true));
verify(strategy, never()).getContext();
verify(originalSupplier, never()).get();
}
private class MockFilterChain implements FilterChain { private class MockFilterChain implements FilterChain {
private boolean expectToProceed; private boolean expectToProceed;