Merge branch '6.0.x'

This commit is contained in:
rstoyanchev 2023-11-16 11:15:44 +00:00
commit af1b3c72d5
1 changed files with 56 additions and 38 deletions

View File

@ -30,7 +30,6 @@ import java.util.stream.Collectors;
import jakarta.servlet.DispatcherType; import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter; import jakarta.servlet.Filter;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletRequestWrapper;
@ -75,15 +74,14 @@ import org.springframework.web.util.pattern.PathPatternParser;
* <p>Note that this is primarily an SPI to allow Spring Security * <p>Note that this is primarily an SPI to allow Spring Security
* to align its pattern matching with the same pattern matching that would be * to align its pattern matching with the same pattern matching that would be
* used in Spring MVC for a given request, in order to avoid security issues. * used in Spring MVC for a given request, in order to avoid security issues.
* Use of this introspector should be avoided for other purposes because it
* incurs the overhead of resolving the handler for a request.
* *
* <p>Alternative security filter solutions that also rely on * <p>Use of this component incurs the performance overhead of mapping the
* {@link HandlerMappingIntrospector} should consider adding an additional * request, and should not be repeated multiple times per request.
* {@link jakarta.servlet.Filter} that invokes * {@link #createCacheFilter()} exposes a Filter to cache the results.
* {@link #setCache(HttpServletRequest)} and {@link #resetCache(ServletRequest, CachedResult)} * Applications that rely on Spring Security don't need to deploy this Filter
* before and after delegating to the rest of the chain. Such a Filter should * since Spring Security doe that. However, other custom security layers, used
* process all dispatcher types and should be ordered ahead of security filters. * in place of Spring Security that use this component should deploy the cache
* Filter with requirements described in the Javadoc for the method.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 4.3.1 * @since 4.3.1
@ -192,16 +190,9 @@ public class HandlerMappingIntrospector
*/ */
public Filter createCacheFilter() { public Filter createCacheFilter() {
return (request, response, chain) -> { return (request, response, chain) -> {
HandlerMappingIntrospector.CachedResult previous = setCache((HttpServletRequest) request); CachedResult previous = setCache((HttpServletRequest) request);
try {
chain.doFilter(request, response); chain.doFilter(request, response);
}
catch (Exception ex) {
throw new ServletException("HandlerMapping introspection failed", ex);
}
finally {
resetCache(request, previous); resetCache(request, previous);
}
}; };
} }
@ -212,26 +203,39 @@ public class HandlerMappingIntrospector
* {@link #getCorsConfiguration(HttpServletRequest)} to avoid repeated lookups. * {@link #getCorsConfiguration(HttpServletRequest)} to avoid repeated lookups.
* @param request the current request * @param request the current request
* @return the previous {@link CachedResult}, if there is one from a parent dispatch * @return the previous {@link CachedResult}, if there is one from a parent dispatch
* @throws ServletException thrown the lookup fails for any reason
* @since 6.0.14 * @since 6.0.14
*/ */
@Nullable @Nullable
private CachedResult setCache(HttpServletRequest request) throws ServletException { private CachedResult setCache(HttpServletRequest request) {
CachedResult previous = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE); CachedResult previous = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE);
if (previous == null || !previous.matches(request)) { if (previous == null || !previous.matches(request)) {
try {
HttpServletRequest wrapped = new AttributesPreservingRequest(request); HttpServletRequest wrapped = new AttributesPreservingRequest(request);
CachedResult result = doWithHandlerMapping(wrapped, false, (mapping, executionChain) -> { CachedResult result;
try {
// Try to get both in one lookup (with ignoringException=false)
result = doWithHandlerMapping(wrapped, false, (mapping, executionChain) -> {
MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping(mapping, wrapped); MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping(mapping, wrapped);
CorsConfiguration corsConfig = getCorsConfiguration(wrapped, executionChain); CorsConfiguration corsConfig = getCorsConfiguration(executionChain, wrapped);
return new CachedResult(request, matchableMapping, corsConfig); return new CachedResult(request, matchableMapping, corsConfig, null, null);
}); });
request.setAttribute(CACHED_RESULT_ATTRIBUTE,
(result != null ? result : new CachedResult(request, null, null)));
} }
catch (Throwable ex) { catch (Exception ex) {
throw new ServletException("HandlerMapping introspection failed", ex); try {
// Try CorsConfiguration at least with ignoreException=true
AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request);
result = doWithHandlerMapping(requestToUse, true, (mapping, executionChain) -> {
CorsConfiguration corsConfig = getCorsConfiguration(executionChain, wrapped);
return new CachedResult(request, null, corsConfig, ex, null);
});
} }
catch (Exception ex2) {
result = new CachedResult(request, null, null, ex, new IllegalStateException(ex2));
}
}
if (result == null) {
result = new CachedResult(request, null, null, null, null);
}
request.setAttribute(CACHED_RESULT_ATTRIBUTE, result);
} }
return previous; return previous;
} }
@ -256,7 +260,7 @@ public class HandlerMappingIntrospector
*/ */
@Nullable @Nullable
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
CachedResult result = CachedResult.forRequest(request); CachedResult result = CachedResult.getResultFor(request);
if (result != null) { if (result != null) {
return result.getHandlerMapping(); return result.getHandlerMapping();
} }
@ -284,7 +288,7 @@ public class HandlerMappingIntrospector
@Override @Override
@Nullable @Nullable
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CachedResult result = CachedResult.forRequest(request); CachedResult result = CachedResult.getResultFor(request);
if (result != null) { if (result != null) {
return result.getCorsConfig(); return result.getCorsConfig();
} }
@ -293,16 +297,16 @@ public class HandlerMappingIntrospector
boolean ignoreException = true; boolean ignoreException = true;
AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request); AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request);
return doWithHandlerMapping(requestToUse, ignoreException, return doWithHandlerMapping(requestToUse, ignoreException,
(handlerMapping, executionChain) -> getCorsConfiguration(requestToUse, executionChain)); (handlerMapping, executionChain) -> getCorsConfiguration(executionChain, requestToUse));
} }
catch (Exception ex) { catch (Exception ex) {
// HandlerMapping exceptions have been ignored. Some more basic error perhaps like request parsing // HandlerMapping exceptions are ignored. More basic error like parsing the request path.
throw new IllegalStateException(ex); throw new IllegalStateException(ex);
} }
} }
@Nullable @Nullable
private static CorsConfiguration getCorsConfiguration(HttpServletRequest request, HandlerExecutionChain chain) { private static CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, HttpServletRequest request) {
for (HandlerInterceptor interceptor : chain.getInterceptorList()) { for (HandlerInterceptor interceptor : chain.getInterceptorList()) {
if (interceptor instanceof CorsConfigurationSource source) { if (interceptor instanceof CorsConfigurationSource source) {
return source.getCorsConfiguration(request); return source.getCorsConfiguration(request);
@ -371,13 +375,22 @@ public class HandlerMappingIntrospector
@Nullable @Nullable
private final CorsConfiguration corsConfig; private final CorsConfiguration corsConfig;
@Nullable
private final Exception failure;
@Nullable
private final IllegalStateException corsConfigFailure;
private CachedResult(HttpServletRequest request, private CachedResult(HttpServletRequest request,
@Nullable MatchableHandlerMapping mapping, @Nullable CorsConfiguration config) { @Nullable MatchableHandlerMapping mapping, @Nullable CorsConfiguration config,
@Nullable Exception failure, @Nullable IllegalStateException corsConfigFailure) {
this.dispatcherType = request.getDispatcherType(); this.dispatcherType = request.getDispatcherType();
this.requestURI = request.getRequestURI(); this.requestURI = request.getRequestURI();
this.handlerMapping = mapping; this.handlerMapping = mapping;
this.corsConfig = config; this.corsConfig = config;
this.failure = failure;
this.corsConfigFailure = corsConfigFailure;
} }
public boolean matches(HttpServletRequest request) { public boolean matches(HttpServletRequest request) {
@ -386,12 +399,18 @@ public class HandlerMappingIntrospector
} }
@Nullable @Nullable
public MatchableHandlerMapping getHandlerMapping() { public MatchableHandlerMapping getHandlerMapping() throws Exception {
if (this.failure != null) {
throw this.failure;
}
return this.handlerMapping; return this.handlerMapping;
} }
@Nullable @Nullable
public CorsConfiguration getCorsConfig() { public CorsConfiguration getCorsConfig() {
if (this.corsConfigFailure != null) {
throw this.corsConfigFailure;
}
return this.corsConfig; return this.corsConfig;
} }
@ -405,11 +424,10 @@ public class HandlerMappingIntrospector
* Return a {@link CachedResult} that matches the given request. * Return a {@link CachedResult} that matches the given request.
*/ */
@Nullable @Nullable
public static CachedResult forRequest(HttpServletRequest request) { public static CachedResult getResultFor(HttpServletRequest request) {
CachedResult result = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE); CachedResult result = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE);
return (result != null && result.matches(request) ? result : null); return (result != null && result.matches(request) ? result : null);
} }
} }