Skip to content

Commit

Permalink
Merge branch 'spring-projects:main' into spring-projectsgh-16251
Browse files Browse the repository at this point in the history
  • Loading branch information
kwondh5217 authored Dec 20, 2024
2 parents a3a8f23 + c72359b commit 7146c46
Show file tree
Hide file tree
Showing 55 changed files with 1,578 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,45 @@

import java.util.List;

import jakarta.servlet.Filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
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;

/**
* A filter chain validator for filter chains built by {@link WebSecurity}
*
* @author Josh Cummings
* @author Max Batischev
* @since 6.5
*/
final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterChainValidator {

private final Log logger = LogFactory.getLog(getClass());

@Override
public void validate(FilterChainProxy filterChainProxy) {
List<SecurityFilterChain> chains = filterChainProxy.getFilterChains();
checkForAnyRequestRequestMatcher(chains);
checkForDuplicateMatchers(chains);
checkAuthorizationFilters(chains);
}

private void checkForAnyRequestRequestMatcher(List<SecurityFilterChain> chains) {
DefaultSecurityFilterChain anyRequestFilterChain = null;
for (SecurityFilterChain chain : chains) {
if (anyRequestFilterChain != null) {
String message = "A filter chain that matches any request [" + anyRequestFilterChain
+ "] has already been configured, which means that this filter chain [" + chain
+ "] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last.";
throw new IllegalArgumentException(message);
throw new UnreachableFilterChainException(message, anyRequestFilterChain, chain);
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher() instanceof AnyRequestMatcher) {
Expand All @@ -49,4 +66,48 @@ public void validate(FilterChainProxy filterChainProxy) {
}
}

private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
DefaultSecurityFilterChain filterChain = null;
for (SecurityFilterChain chain : chains) {
if (filterChain != null) {
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) {
throw new UnreachableFilterChainException(
"The FilterChainProxy contains two filter chains using the" + " matcher "
+ defaultChain.getRequestMatcher(),
filterChain, defaultChain);
}
}
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
filterChain = defaultChain;
}
}
}

private void checkAuthorizationFilters(List<SecurityFilterChain> chains) {
Filter authorizationFilter = null;
Filter filterSecurityInterceptor = null;
for (SecurityFilterChain chain : chains) {
for (Filter filter : chain.getFilters()) {
if (filter instanceof AuthorizationFilter) {
authorizationFilter = filter;
}
if (filter instanceof FilterSecurityInterceptor) {
filterSecurityInterceptor = filter;
}
}
if (authorizationFilter != null && filterSecurityInterceptor != null) {
this.logger.warn(
"It is not recommended to use authorizeRequests in the configuration. Please only use authorizeHttpRequests");
}
if (filterSecurityInterceptor != null) {
this.logger.warn(
"Usage of authorizeRequests is deprecated. Please use authorizeHttpRequests in the configuration");
}
authorizationFilter = null;
filterSecurityInterceptor = null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.authentication.session.SessionLimit;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.NullSecurityContextRepository;
Expand Down Expand Up @@ -123,7 +124,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>

private SessionRegistry sessionRegistry;

private Integer maximumSessions;
private SessionLimit sessionLimit;

private String expiredUrl;

Expand Down Expand Up @@ -329,7 +330,7 @@ public SessionManagementConfigurer<H> sessionFixation(
* @return the {@link SessionManagementConfigurer} for further customizations
*/
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
this.maximumSessions = maximumSessions;
this.sessionLimit = SessionLimit.of(maximumSessions);
this.propertiesThatRequireImplicitAuthentication.add("maximumSessions = " + maximumSessions);
return new ConcurrencyControlConfigurer();
}
Expand Down Expand Up @@ -570,7 +571,7 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
SessionRegistry sessionRegistry = getSessionRegistry(http);
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
sessionRegistry);
concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
concurrentSessionControlStrategy.setMaximumSessions(this.sessionLimit);
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
Expand Down Expand Up @@ -614,7 +615,7 @@ private void registerDelegateApplicationListener(H http, ApplicationListener<?>
* @return
*/
private boolean isConcurrentSessionControlEnabled() {
return this.maximumSessions != null;
return this.sessionLimit != null;
}

/**
Expand Down Expand Up @@ -706,7 +707,19 @@ private ConcurrencyControlConfigurer() {
* @return the {@link ConcurrencyControlConfigurer} for further customizations
*/
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
SessionManagementConfigurer.this.maximumSessions = maximumSessions;
SessionManagementConfigurer.this.sessionLimit = SessionLimit.of(maximumSessions);
return this;
}

/**
* Determines the behaviour when a session limit is detected.
* @param sessionLimit the {@link SessionLimit} to check the maximum number of
* sessions for a user
* @return the {@link ConcurrencyControlConfigurer} for further customizations
* @since 6.5
*/
public ConcurrencyControlConfigurer maximumSessions(SessionLimit sessionLimit) {
SessionManagementConfigurer.this.sessionLimit = sessionLimit;
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
Expand All @@ -53,7 +54,6 @@
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.security.web.util.matcher.RequestMatcher;

public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {

Expand All @@ -69,31 +69,67 @@ public void validate(FilterChainProxy fcp) {
}
checkPathOrder(new ArrayList<>(fcp.getFilterChains()));
checkForDuplicateMatchers(new ArrayList<>(fcp.getFilterChains()));
checkAuthorizationFilters(new ArrayList<>(fcp.getFilterChains()));
}

private void checkPathOrder(List<SecurityFilterChain> filterChains) {
// Check that the universal pattern is listed at the end, if at all
Iterator<SecurityFilterChain> chains = filterChains.iterator();
while (chains.hasNext()) {
RequestMatcher matcher = ((DefaultSecurityFilterChain) chains.next()).getRequestMatcher();
if (AnyRequestMatcher.INSTANCE.equals(matcher) && chains.hasNext()) {
throw new IllegalArgumentException("A universal match pattern ('/**') is defined "
+ " before other patterns in the filter chain, causing them to be ignored. Please check the "
+ "ordering in your <security:http> namespace or FilterChainProxy bean configuration");
if (chains.next() instanceof DefaultSecurityFilterChain securityFilterChain) {
if (AnyRequestMatcher.INSTANCE.equals(securityFilterChain.getRequestMatcher()) && chains.hasNext()) {
throw new UnreachableFilterChainException("A universal match pattern ('/**') is defined "
+ " before other patterns in the filter chain, causing them to be ignored. Please check the "
+ "ordering in your <security:http> namespace or FilterChainProxy bean configuration",
securityFilterChain, chains.next());
}
}
}
}

private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
while (chains.size() > 1) {
DefaultSecurityFilterChain chain = (DefaultSecurityFilterChain) chains.remove(0);
for (SecurityFilterChain test : chains) {
if (chain.getRequestMatcher().equals(((DefaultSecurityFilterChain) test).getRequestMatcher())) {
throw new IllegalArgumentException("The FilterChainProxy contains two filter chains using the"
+ " matcher " + chain.getRequestMatcher() + ". If you are using multiple <http> namespace "
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.");
DefaultSecurityFilterChain filterChain = null;
for (SecurityFilterChain chain : chains) {
if (filterChain != null) {
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) {
throw new UnreachableFilterChainException(
"The FilterChainProxy contains two filter chains using the" + " matcher "
+ defaultChain.getRequestMatcher()
+ ". If you are using multiple <http> namespace "
+ "elements, you must use a 'pattern' attribute to define the request patterns to which they apply.",
defaultChain, chain);
}
}
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
filterChain = defaultChain;
}
}
}

private void checkAuthorizationFilters(List<SecurityFilterChain> chains) {
Filter authorizationFilter = null;
Filter filterSecurityInterceptor = null;
for (SecurityFilterChain chain : chains) {
for (Filter filter : chain.getFilters()) {
if (filter instanceof AuthorizationFilter) {
authorizationFilter = filter;
}
if (filter instanceof FilterSecurityInterceptor) {
filterSecurityInterceptor = filter;
}
}
if (authorizationFilter != null && filterSecurityInterceptor != null) {
this.logger.warn(
"It is not recommended to use authorizeRequests in the configuration. Please only use authorizeHttpRequests");
}
if (filterSecurityInterceptor != null) {
this.logger.warn(
"Usage of authorizeRequests is deprecated. Please use authorizeHttpRequests in the configuration");
}
authorizationFilter = null;
filterSecurityInterceptor = null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ class HttpConfigurationBuilder {

private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";

private static final String ATT_MAX_SESSIONS_REF = "max-sessions-ref";

private static final String ATT_MAX_SESSIONS = "max-sessions";

private static final String ATT_SESSION_AUTH_ERROR_URL = "session-authentication-error-url";

private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY = "security-context-holder-strategy-ref";
Expand Down Expand Up @@ -485,10 +489,16 @@ else if (StringUtils.hasText(sessionAuthStratRef)) {
concurrentSessionStrategy.addConstructorArgValue(this.sessionRegistryRef);
String maxSessions = this.pc.getReaderContext()
.getEnvironment()
.resolvePlaceholders(sessionCtrlElt.getAttribute("max-sessions"));
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS));
if (StringUtils.hasText(maxSessions)) {
concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions);
}
String maxSessionsRef = this.pc.getReaderContext()
.getEnvironment()
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS_REF));
if (StringUtils.hasText(maxSessionsRef)) {
concurrentSessionStrategy.addPropertyReference("maximumSessions", maxSessionsRef);
}
String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded");
if (StringUtils.hasText(exceptionIfMaximumExceeded)) {
concurrentSessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded);
Expand Down Expand Up @@ -591,6 +601,12 @@ private void createConcurrencyControlFilterAndSessionRegistry(Element element) {
.error("Cannot use 'expired-url' attribute and 'expired-session-strategy-ref'" + " attribute together.",
source);
}
String maxSessions = element.getAttribute(ATT_MAX_SESSIONS);
String maxSessionsRef = element.getAttribute(ATT_MAX_SESSIONS_REF);
if (StringUtils.hasText(maxSessions) && StringUtils.hasText(maxSessionsRef)) {
this.pc.getReaderContext()
.error("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.", source);
}
if (StringUtils.hasText(expiryUrl)) {
BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder
.rootBeanDefinition(SimpleRedirectSessionInformationExpiredStrategy.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public BeanDefinition parse(Element element, ParserContext pc) {
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
.addConstructorArgValue(logoutRequestResolver)
.addPropertyValue("logoutRequestRepository", logoutRequestRepository)
.getBeanDefinition();
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
.addConstructorArgValue(saml2LogoutRequestSuccessHandler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package org.springframework.security.config.annotation.web.session
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.web.authentication.session.SessionLimit
import org.springframework.security.web.session.SessionInformationExpiredStrategy
import org.springframework.util.Assert

/**
* A Kotlin DSL to configure the behaviour of multiple sessions using idiomatic
Expand All @@ -44,12 +46,21 @@ class SessionConcurrencyDsl {
var expiredSessionStrategy: SessionInformationExpiredStrategy? = null
var maxSessionsPreventsLogin: Boolean? = null
var sessionRegistry: SessionRegistry? = null
private var sessionLimit: SessionLimit? = null

fun maximumSessions(max: SessionLimit) {
this.sessionLimit = max
}

internal fun get(): (SessionManagementConfigurer<HttpSecurity>.ConcurrencyControlConfigurer) -> Unit {
Assert.isTrue(maximumSessions == null || sessionLimit == null, "You cannot specify maximumSessions as both an Int and a SessionLimit. Please use only one.")
return { sessionConcurrencyControl ->
maximumSessions?.also {
sessionConcurrencyControl.maximumSessions(maximumSessions!!)
}
sessionLimit?.also {
sessionConcurrencyControl.maximumSessions(sessionLimit!!)
}
expiredUrl?.also {
sessionConcurrencyControl.expiredUrl(expiredUrl)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,9 @@ concurrency-control =
concurrency-control.attlist &=
## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions.
attribute max-sessions {xsd:token}?
concurrency-control.attlist &=
## Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy
attribute max-sessions-ref {xsd:token}?
concurrency-control.attlist &=
## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again.
attribute expired-url {xsd:token}?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2688,6 +2688,13 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="max-sessions-ref" type="xs:token">
<xs:annotation>
<xs:documentation>Allows injection of the SessionLimit instance used by the
ConcurrentSessionControlAuthenticationStrategy
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="expired-url" type="xs:token">
<xs:annotation>
<xs:documentation>The URL a user will be redirected to if they attempt to use a session which has been
Expand Down
Loading

0 comments on commit 7146c46

Please sign in to comment.