27
27
import java .util .function .BiFunction ;
28
28
import java .util .stream .Collectors ;
29
29
30
- import jakarta .servlet .Filter ;
30
+ import jakarta .servlet .DispatcherType ;
31
31
import jakarta .servlet .ServletException ;
32
32
import jakarta .servlet .ServletRequest ;
33
33
import jakarta .servlet .http .HttpServletRequest ;
68
68
* request.
69
69
* </ul>
70
70
*
71
- * <p><strong> Note:</strong> This is primarily an SPI to allow Spring Security
71
+ * <p>Note that this is primarily an SPI to allow Spring Security
72
72
* to align its pattern matching with the same pattern matching that would be
73
73
* used in Spring MVC for a given request, in order to avoid security issues.
74
74
* Use of this introspector should be avoided for other purposes because it
75
75
* incurs the overhead of resolving the handler for a request.
76
76
*
77
+ * <p>Alternative security filter solutions that also rely on
78
+ * {@link HandlerMappingIntrospector} should consider adding an additional
79
+ * {@link jakarta.servlet.Filter} that invokes
80
+ * {@link #setCache(HttpServletRequest)} and {@link #resetCache(ServletRequest, CachedResult)}
81
+ * before and after delegating to the rest of the chain. Such a Filter should
82
+ * process all dispatcher types and should be ordered ahead of security filters.
83
+ *
77
84
* @author Rossen Stoyanchev
78
85
* @since 4.3.1
79
86
*/
80
87
public class HandlerMappingIntrospector
81
88
implements CorsConfigurationSource , ApplicationContextAware , InitializingBean {
82
89
83
- static final String MAPPING_ATTRIBUTE =
84
- HandlerMappingIntrospector .class .getName () + ".HandlerMapping" ;
85
-
86
- static final String CORS_CONFIG_ATTRIBUTE =
87
- HandlerMappingIntrospector .class .getName () + ".CorsConfig" ;
88
-
89
- private static final CorsConfiguration NO_CORS_CONFIG = new CorsConfiguration ();
90
+ private static final String CACHED_RESULT_ATTRIBUTE =
91
+ HandlerMappingIntrospector .class .getName () + ".CachedResult" ;
90
92
91
93
92
94
@ Nullable
@@ -166,55 +168,43 @@ public List<HandlerMapping> getHandlerMappings() {
166
168
167
169
168
170
/**
169
- * Return Filter that performs lookups, caches the results in request attributes,
170
- * and clears the attributes after the filter chain returns.
171
+ * Perform a lookup and save the {@link CachedResult} as a request attribute.
172
+ * This method can be invoked from a filter before subsequent calls to
173
+ * {@link #getMatchableHandlerMapping(HttpServletRequest)} and
174
+ * {@link #getCorsConfiguration(HttpServletRequest)} to avoid repeated lookups.
175
+ * @param request the current request
176
+ * @return the previous {@link CachedResult}, if there is one from a parent dispatch
177
+ * @throws ServletException thrown the lookup fails for any reason
171
178
* @since 6.0.14
172
179
*/
173
- public Filter createCacheFilter () {
174
- return ( request , response , chain ) -> {
175
- MatchableHandlerMapping previousMapping = getCachedMapping (request );
176
- CorsConfiguration previousCorsConfig = getCachedCorsConfiguration (request );
180
+ @ Nullable
181
+ public CachedResult setCache ( HttpServletRequest request ) throws ServletException {
182
+ CachedResult previous = getAttribute (request );
183
+ if ( previous == null || ! previous . matches (request )) {
177
184
try {
178
- HttpServletRequest wrappedRequest = new AttributesPreservingRequest ((HttpServletRequest ) request );
179
- doWithHandlerMapping (wrappedRequest , false , (mapping , executionChain ) -> {
180
- MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping (mapping , wrappedRequest );
181
- CorsConfiguration corsConfig = getCorsConfiguration (wrappedRequest , executionChain );
182
- setCache (request , matchableMapping , corsConfig );
183
- return null ;
185
+ HttpServletRequest wrapped = new AttributesPreservingRequest (request );
186
+ CachedResult cachedResult = doWithHandlerMapping (wrapped , false , (mapping , executionChain ) -> {
187
+ MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping (mapping , wrapped );
188
+ CorsConfiguration corsConfig = getCorsConfiguration (wrapped , executionChain );
189
+ return new CachedResult (request , matchableMapping , corsConfig );
184
190
});
185
- chain .doFilter (request , response );
191
+ request .setAttribute (CACHED_RESULT_ATTRIBUTE ,
192
+ cachedResult != null ? cachedResult : new CachedResult (request , null , null ));
186
193
}
187
- catch (Exception ex ) {
194
+ catch (Throwable ex ) {
188
195
throw new ServletException ("HandlerMapping introspection failed" , ex );
189
196
}
190
- finally {
191
- setCache (request , previousMapping , previousCorsConfig );
192
- }
193
- };
194
- }
195
-
196
- @ Nullable
197
- private static MatchableHandlerMapping getCachedMapping (ServletRequest request ) {
198
- return (MatchableHandlerMapping ) request .getAttribute (MAPPING_ATTRIBUTE );
199
- }
200
-
201
- @ Nullable
202
- private static CorsConfiguration getCachedCorsConfiguration (ServletRequest request ) {
203
- return (CorsConfiguration ) request .getAttribute (CORS_CONFIG_ATTRIBUTE );
197
+ }
198
+ return previous ;
204
199
}
205
200
206
- private static void setCache (
207
- ServletRequest request , @ Nullable MatchableHandlerMapping mapping ,
208
- @ Nullable CorsConfiguration corsConfig ) {
209
-
210
- if (mapping != null ) {
211
- request .setAttribute (MAPPING_ATTRIBUTE , mapping );
212
- request .setAttribute (CORS_CONFIG_ATTRIBUTE , (corsConfig != null ? corsConfig : NO_CORS_CONFIG ));
213
- }
214
- else {
215
- request .removeAttribute (MAPPING_ATTRIBUTE );
216
- request .removeAttribute (CORS_CONFIG_ATTRIBUTE );
217
- }
201
+ /**
202
+ * Restore a previous {@link CachedResult}. This method can be invoked from
203
+ * a filter after delegating to the rest of the chain.
204
+ * @since 6.0.14
205
+ */
206
+ public void resetCache (ServletRequest request , @ Nullable CachedResult cachedResult ) {
207
+ request .setAttribute (CACHED_RESULT_ATTRIBUTE , cachedResult );
218
208
}
219
209
220
210
/**
@@ -228,9 +218,9 @@ private static void setCache(
228
218
*/
229
219
@ Nullable
230
220
public MatchableHandlerMapping getMatchableHandlerMapping (HttpServletRequest request ) throws Exception {
231
- MatchableHandlerMapping cachedMapping = getCachedMapping (request );
232
- if (cachedMapping != null ) {
233
- return cachedMapping ;
221
+ CachedResult cachedResult = getCachedResultFor (request );
222
+ if (cachedResult != null ) {
223
+ return cachedResult . getHandlerMapping () ;
234
224
}
235
225
HttpServletRequest requestToUse = new AttributesPreservingRequest (request );
236
226
return doWithHandlerMapping (requestToUse , false ,
@@ -255,9 +245,9 @@ private MatchableHandlerMapping createMatchableHandlerMapping(HandlerMapping map
255
245
@ Override
256
246
@ Nullable
257
247
public CorsConfiguration getCorsConfiguration (HttpServletRequest request ) {
258
- CorsConfiguration cachedCorsConfiguration = getCachedCorsConfiguration (request );
259
- if (cachedCorsConfiguration != null ) {
260
- return ( cachedCorsConfiguration != NO_CORS_CONFIG ? cachedCorsConfiguration : null );
248
+ CachedResult cachedResult = getCachedResultFor (request );
249
+ if (cachedResult != null ) {
250
+ return cachedResult . getCorsConfig ( );
261
251
}
262
252
try {
263
253
boolean ignoreException = true ;
@@ -322,6 +312,68 @@ private <T> T doWithHandlerMapping(
322
312
return null ;
323
313
}
324
314
315
+ /**
316
+ * Return a {@link CachedResult} that matches the given request.
317
+ */
318
+ @ Nullable
319
+ private CachedResult getCachedResultFor (HttpServletRequest request ) {
320
+ CachedResult result = getAttribute (request );
321
+ return (result != null && result .matches (request ) ? result : null );
322
+ }
323
+
324
+ @ Nullable
325
+ private static CachedResult getAttribute (HttpServletRequest request ) {
326
+ return (CachedResult ) request .getAttribute (CACHED_RESULT_ATTRIBUTE );
327
+ }
328
+
329
+
330
+ /**
331
+ * Container for a {@link MatchableHandlerMapping} and {@link CorsConfiguration}
332
+ * for a given request identified by dispatcher type and requestURI.
333
+ * @since 6.0.14
334
+ */
335
+ public final static class CachedResult {
336
+
337
+ private final DispatcherType dispatcherType ;
338
+
339
+ private final String requestURI ;
340
+
341
+ @ Nullable
342
+ private final MatchableHandlerMapping handlerMapping ;
343
+
344
+ @ Nullable
345
+ private final CorsConfiguration corsConfig ;
346
+
347
+ private CachedResult (HttpServletRequest request ,
348
+ @ Nullable MatchableHandlerMapping mapping , @ Nullable CorsConfiguration config ) {
349
+
350
+ this .dispatcherType = request .getDispatcherType ();
351
+ this .requestURI = request .getRequestURI ();
352
+ this .handlerMapping = mapping ;
353
+ this .corsConfig = config ;
354
+ }
355
+
356
+ public boolean matches (HttpServletRequest request ) {
357
+ return (this .dispatcherType .equals (request .getDispatcherType ()) &&
358
+ this .requestURI .matches (request .getRequestURI ()));
359
+ }
360
+
361
+ @ Nullable
362
+ public MatchableHandlerMapping getHandlerMapping () {
363
+ return this .handlerMapping ;
364
+ }
365
+
366
+ @ Nullable
367
+ public CorsConfiguration getCorsConfig () {
368
+ return this .corsConfig ;
369
+ }
370
+
371
+ @ Override
372
+ public String toString () {
373
+ return "CacheValue " + this .dispatcherType + " '" + this .requestURI + "'" ;
374
+ }
375
+ }
376
+
325
377
326
378
/**
327
379
* Request wrapper that buffers request attributes in order protect the
0 commit comments