diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java index 89e2fe007e..063ced5e88 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java @@ -23,9 +23,7 @@ import java.util.Map; import jakarta.servlet.Filter; import org.springframework.security.web.access.ExceptionTranslationFilter; -import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.access.intercept.AuthorizationFilter; -import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.AuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -78,7 +76,7 @@ final class FilterOrderRegistration { Step order = new Step(INITIAL_ORDER, ORDER_STEP); put(DisableEncodeUrlFilter.class, order.next()); put(ForceEagerSessionCreationFilter.class, order.next()); - put(ChannelProcessingFilter.class, order.next()); + this.filterToOrder.put("org.springframework.security.web.access.channel.ChannelProcessingFilter", order.next()); put(HttpsRedirectFilter.class, order.next()); order.next(); // gh-8105 put(WebAsyncManagerIntegrationFilter.class, order.next()); @@ -126,7 +124,8 @@ final class FilterOrderRegistration { order.next()); put(SessionManagementFilter.class, order.next()); put(ExceptionTranslationFilter.class, order.next()); - put(FilterSecurityInterceptor.class, order.next()); + this.filterToOrder.put("org.springframework.security.web.access.intercept.FilterSecurityInterceptor", + order.next()); put(AuthorizationFilter.class, order.next()); put(SwitchUserFilter.class, order.next()); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 943cfbc802..bce1b3bdf2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.builders; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.Filter; @@ -25,6 +26,8 @@ import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -32,8 +35,11 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.ResolvableType; +import org.springframework.expression.EvaluationContext; import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.access.expression.SecurityExpressionOperations; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; @@ -46,6 +52,7 @@ import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; @@ -58,7 +65,7 @@ import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEval import org.springframework.security.web.access.PathPatternRequestTransformer; import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; -import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; +import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; @@ -74,6 +81,7 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.web.context.ServletContextAware; import org.springframework.web.filter.DelegatingFilterProxy; @@ -99,6 +107,9 @@ import org.springframework.web.filter.DelegatingFilterProxy; public final class WebSecurity extends AbstractConfiguredSecurityBuilder implements SecurityBuilder, ApplicationContextAware, ServletContextAware { + private static final boolean USING_ACCESS = ClassUtils + .isPresent("org.springframework.security.access.SecurityConfig", null); + private final Log logger = LogFactory.getLog(getClass()); private final List ignoredRequests = new ArrayList<>(); @@ -122,9 +133,10 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder expressionHandler = this.defaultWebSecurityExpressionHandler; + private SecurityExpressionHandler expressionHandler = new SecurityExpressionHandlerAdapter( + this.defaultExpressionHandler); private Runnable postBuildAction = () -> { }; @@ -240,7 +252,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder authorizationManager = (authentication, context) -> { - HttpServletRequest request = context.getRequest(); - boolean result = privilegeEvaluator.isAllowed(request.getContextPath(), request.getRequestURI(), - request.getMethod(), authentication.get()); - return new AuthorizationDecision(result); - }; - builder.add(securityFilterChain::matches, authorizationManager); - mappings = true; - continue; + if (USING_ACCESS) { + mappings = AccessComponents.addAuthorizationManager(filter, this.servletContext, builder, + securityFilterChain); } if (filter instanceof AuthorizationFilter authorization) { AuthorizationManager authorizationManager = authorization.getAuthorizationManager(); @@ -388,15 +390,14 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder { + + private final AbstractSecurityExpressionHandler delegate; + + private SecurityExpressionHandlerAdapter( + AbstractSecurityExpressionHandler delegate) { + this.delegate = delegate; + } + + @Override + public EvaluationContext createEvaluationContext(Supplier authentication, + FilterInvocation invocation) { + RequestAuthorizationContext context = new RequestAuthorizationContext(invocation.getRequest()); + return this.delegate.createEvaluationContext(authentication, context); + } + + @Override + protected SecurityExpressionOperations createSecurityExpressionRoot(@Nullable Authentication authentication, + FilterInvocation invocation) { + RequestAuthorizationContext context = new RequestAuthorizationContext(invocation.getRequest()); + Object operations = this.delegate.createEvaluationContext(authentication, context) + .getRootObject() + .getValue(); + Assert.isInstanceOf(SecurityExpressionOperations.class, operations, + "createEvaluationContext must have a SecurityExpressionOperations instance as its root"); + return (SecurityExpressionOperations) operations; + } + + @Override + public void setApplicationContext(ApplicationContext context) { + this.delegate.setApplicationContext(context); + super.setApplicationContext(context); + } + + @Override + public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { + this.delegate.setPermissionEvaluator(permissionEvaluator); + super.setPermissionEvaluator(permissionEvaluator); + } + + @Override + public void setRoleHierarchy(@Nullable RoleHierarchy roleHierarchy) { + this.delegate.setRoleHierarchy(roleHierarchy); + super.setRoleHierarchy(roleHierarchy); + } + + } + + private static final class AccessComponents { + + private static boolean addAuthorizationManager(Filter filter, ServletContext servletContext, + RequestMatcherDelegatingAuthorizationManager.Builder builder, SecurityFilterChain securityFilterChain) { + if (filter instanceof FilterSecurityInterceptor securityInterceptor) { + DefaultWebInvocationPrivilegeEvaluator privilegeEvaluator = new DefaultWebInvocationPrivilegeEvaluator( + securityInterceptor); + privilegeEvaluator.setServletContext(servletContext); + AuthorizationManager authorizationManager = (authentication, context) -> { + HttpServletRequest request = context.getRequest(); + boolean result = privilegeEvaluator.isAllowed(request.getContextPath(), request.getRequestURI(), + request.getMethod(), authentication.get()); + return new AuthorizationDecision(result); + }; + builder.add(securityFilterChain::matches, authorizationManager); + return true; + } + return false; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java index 971b2cf810..fa97043fb6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java @@ -29,6 +29,7 @@ import org.springframework.security.web.UnreachableFilterChainException; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.util.ClassUtils; /** * A filter chain validator for filter chains built by {@link WebSecurity} @@ -39,6 +40,9 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher; */ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterChainValidator { + private static final boolean USING_ACCESS = ClassUtils + .isPresent("org.springframework.security.access.SecurityConfig", null); + private final Log logger = LogFactory.getLog(getClass()); @Override @@ -93,7 +97,7 @@ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterCh if (filter instanceof AuthorizationFilter) { authorizationFilter = filter; } - if (filter instanceof FilterSecurityInterceptor) { + if (USING_ACCESS && AccessComponents.isFilterSecurityInterceptor(filter)) { filterSecurityInterceptor = filter; } } @@ -110,4 +114,12 @@ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterCh } } + private static final class AccessComponents { + + private static boolean isFilterSecurityInterceptor(Filter filter) { + return filter instanceof FilterSecurityInterceptor; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java index a7dcbbdd98..fb88f14b3a 100644 --- a/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java +++ b/config/src/main/java/org/springframework/security/config/http/DefaultFilterChainValidator.java @@ -56,9 +56,13 @@ import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.util.ClassUtils; public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator { + private static final boolean USING_ACCESS = ClassUtils + .isPresent("org.springframework.security.access.SecurityConfig", null); + private static final Authentication TEST = new TestingAuthenticationToken("", "", Collections.emptyList()); private final Log logger = LogFactory.getLog(getClass()); @@ -120,7 +124,7 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain if (filter instanceof AuthorizationFilter) { authorizationFilter = filter; } - if (filter instanceof FilterSecurityInterceptor) { + if (USING_ACCESS && AccessComponents.isFilterSecurityInterceptor(filter)) { filterSecurityInterceptor = filter; } } @@ -138,7 +142,7 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain } @SuppressWarnings({ "unchecked" }) - private F getFilter(Class type, List filters) { + private static F getFilter(Class type, List filters) { for (Filter f : filters) { if (type.isAssignableFrom(f.getClass())) { return (F) f; @@ -158,7 +162,9 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain checkForDuplicates(SecurityContextHolderAwareRequestFilter.class, filters); checkForDuplicates(JaasApiIntegrationFilter.class, filters); checkForDuplicates(ExceptionTranslationFilter.class, filters); - checkForDuplicates(FilterSecurityInterceptor.class, filters); + if (USING_ACCESS) { + checkForDuplicates(AccessComponents.getFilterSecurityInterceptorClass(), filters); + } checkForDuplicates(AuthorizationFilter.class, filters); } @@ -243,19 +249,11 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain } private boolean checkLoginPageIsPublic(List filters, HttpServletRequest loginRequest) { - FilterSecurityInterceptor authorizationInterceptor = getFilter(FilterSecurityInterceptor.class, filters); - if (authorizationInterceptor != null) { - FilterInvocationSecurityMetadataSource fids = authorizationInterceptor.getSecurityMetadataSource(); - Collection attributes = fids.getAttributes(loginRequest); - if (attributes == null) { - this.logger.debug("No access attributes defined for login page URL"); - if (authorizationInterceptor.isRejectPublicInvocations()) { - this.logger.warn("FilterSecurityInterceptor is configured to reject public invocations." - + " Your login page may not be accessible."); - } - return true; + if (USING_ACCESS) { + Boolean isPublic = AccessComponents.checkLoginPageIsPublic(filters, loginRequest); + if (isPublic != null) { + return isPublic; } - return false; } AuthorizationFilter authorizationFilter = getFilter(AuthorizationFilter.class, filters); if (authorizationFilter != null) { @@ -274,19 +272,11 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain private Supplier deriveAnonymousCheck(List filters, HttpServletRequest loginRequest, AnonymousAuthenticationToken token) { - FilterSecurityInterceptor authorizationInterceptor = getFilter(FilterSecurityInterceptor.class, filters); - if (authorizationInterceptor != null) { - return () -> { - FilterInvocationSecurityMetadataSource source = authorizationInterceptor.getSecurityMetadataSource(); - Collection attributes = source.getAttributes(loginRequest); - try { - authorizationInterceptor.getAccessDecisionManager().decide(token, loginRequest, attributes); - return true; - } - catch (AccessDeniedException ex) { - return false; - } - }; + if (USING_ACCESS) { + Supplier check = AccessComponents.getAnonymousCheck(filters, loginRequest, token); + if (check != null) { + return check; + } } AuthorizationFilter authorizationFilter = getFilter(AuthorizationFilter.class, filters); if (authorizationFilter != null) { @@ -300,4 +290,55 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain return () -> true; } + private static final class AccessComponents { + + private static final Log logger = LogFactory.getLog(DefaultFilterChainValidator.class); + + private static boolean isFilterSecurityInterceptor(Filter filter) { + return filter instanceof FilterSecurityInterceptor; + } + + private static Class getFilterSecurityInterceptorClass() { + return FilterSecurityInterceptor.class; + } + + private static Boolean checkLoginPageIsPublic(List filters, HttpServletRequest loginRequest) { + FilterSecurityInterceptor authorizationInterceptor = getFilter(FilterSecurityInterceptor.class, filters); + if (authorizationInterceptor == null) { + return null; + } + FilterInvocationSecurityMetadataSource fids = authorizationInterceptor.getSecurityMetadataSource(); + Collection attributes = fids.getAttributes(loginRequest); + if (attributes == null) { + logger.debug("No access attributes defined for login page URL"); + if (authorizationInterceptor.isRejectPublicInvocations()) { + logger.warn("FilterSecurityInterceptor is configured to reject public invocations." + + " Your login page may not be accessible."); + } + return true; + } + return false; + } + + private static Supplier getAnonymousCheck(List filters, HttpServletRequest loginRequest, + AnonymousAuthenticationToken token) { + FilterSecurityInterceptor authorizationInterceptor = getFilter(FilterSecurityInterceptor.class, filters); + if (authorizationInterceptor == null) { + return null; + } + return () -> { + FilterInvocationSecurityMetadataSource source = authorizationInterceptor.getSecurityMetadataSource(); + Collection attributes = source.getAttributes(loginRequest); + try { + authorizationInterceptor.getAccessDecisionManager().decide(token, loginRequest, attributes); + return true; + } + catch (AccessDeniedException ex) { + return false; + } + }; + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index 2063563122..922a336e62 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -60,7 +60,6 @@ import org.springframework.security.web.access.AuthorizationManagerWebInvocation import org.springframework.security.web.access.PathPatternRequestTransformer; import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; -import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.test.web.servlet.MockMvc; import org.springframework.util.ClassUtils; @@ -153,7 +152,7 @@ public class WebSecurityConfigurationTests { public void loadConfigWhenDefaultSecurityExpressionHandlerThenDefaultIsRegistered() { this.spring.register(WebSecurityExpressionHandlerDefaultsConfig.class).autowire(); assertThat(this.spring.getContext().getBean(SecurityExpressionHandler.class)) - .isInstanceOf(DefaultWebSecurityExpressionHandler.class); + .isInstanceOf(AbstractSecurityExpressionHandler.class); } @Test diff --git a/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserParentTests.java b/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserParentTests.java index ae6df8c9b9..5887e51381 100644 --- a/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserParentTests.java +++ b/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserParentTests.java @@ -23,7 +23,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.test.context.showcase.service.HelloMessageService; import org.springframework.security.test.context.showcase.service.MessageService; import org.springframework.test.context.ContextConfiguration; @@ -48,7 +49,8 @@ public class WithMockUserParentTests extends WithMockUserParent { } @Configuration - @EnableGlobalMethodSecurity(prePostEnabled = true) + @EnableMethodSecurity + @EnableWebSecurity @ComponentScan(basePackageClasses = HelloMessageService.class) static class Config { diff --git a/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserTests.java b/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserTests.java index 4fb697a878..bdffd8b3e7 100644 --- a/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserTests.java +++ b/test/src/test/java/org/springframework/security/test/context/showcase/WithMockUserTests.java @@ -31,7 +31,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AliasFor; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.test.context.showcase.service.HelloMessageService; import org.springframework.security.test.context.showcase.service.MessageService; import org.springframework.security.test.context.support.WithMockUser; @@ -53,8 +54,8 @@ public class WithMockUserTests { @Test public void getMessageUnauthenticated() { - assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) - .isThrownBy(() -> this.messageService.getMessage()); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> this.messageService.getMessage()) + .withRootCauseInstanceOf(AuthenticationCredentialsNotFoundException.class); } @Test @@ -104,7 +105,8 @@ public class WithMockUserTests { assertThat(message).contains("admin").contains("ADMIN").contains("ROLE_ADMIN"); } - @EnableGlobalMethodSecurity(prePostEnabled = true) + @EnableMethodSecurity + @EnableWebSecurity @ComponentScan(basePackageClasses = HelloMessageService.class) static class Config { diff --git a/test/src/test/java/org/springframework/security/test/context/showcase/WithUserDetailsTests.java b/test/src/test/java/org/springframework/security/test/context/showcase/WithUserDetailsTests.java index 77c403c909..5473335e11 100644 --- a/test/src/test/java/org/springframework/security/test/context/showcase/WithUserDetailsTests.java +++ b/test/src/test/java/org/springframework/security/test/context/showcase/WithUserDetailsTests.java @@ -25,7 +25,8 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -51,8 +52,8 @@ public class WithUserDetailsTests { @Test public void getMessageUnauthenticated() { - assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) - .isThrownBy(() -> this.messageService.getMessage()); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> this.messageService.getMessage()) + .withRootCauseInstanceOf(AuthenticationCredentialsNotFoundException.class); } @Test @@ -84,7 +85,8 @@ public class WithUserDetailsTests { } @Configuration - @EnableGlobalMethodSecurity(prePostEnabled = true) + @EnableMethodSecurity + @EnableWebSecurity @ComponentScan(basePackageClasses = HelloMessageService.class) static class Config {