Skip to content

Commit 99a8717

Browse files
committed
Instrument Filter Chain
Closes gh-11911
1 parent 8c61068 commit 99a8717

File tree

12 files changed

+1798
-21
lines changed

12 files changed

+1798
-21
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121

22+
import io.micrometer.observation.ObservationRegistry;
2223
import jakarta.servlet.Filter;
2324
import jakarta.servlet.ServletContext;
2425
import jakarta.servlet.http.HttpServletRequest;
@@ -45,6 +46,7 @@
4546
import org.springframework.security.web.DefaultSecurityFilterChain;
4647
import org.springframework.security.web.FilterChainProxy;
4748
import org.springframework.security.web.FilterInvocation;
49+
import org.springframework.security.web.ObservationFilterChainDecorator;
4850
import org.springframework.security.web.SecurityFilterChain;
4951
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
5052
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
@@ -101,6 +103,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
101103

102104
private WebInvocationPrivilegeEvaluator privilegeEvaluator;
103105

106+
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
107+
104108
private DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
105109

106110
private SecurityExpressionHandler<FilterInvocation> expressionHandler = this.defaultWebSecurityExpressionHandler;
@@ -303,6 +307,7 @@ protected Filter performBuild() throws Exception {
303307
if (this.requestRejectedHandler != null) {
304308
filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
305309
}
310+
filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
306311
filterChainProxy.afterPropertiesSet();
307312

308313
Filter result = filterChainProxy;
@@ -366,13 +371,25 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
366371
}
367372
catch (NoSuchBeanDefinitionException ex) {
368373
}
374+
try {
375+
this.observationRegistry = applicationContext.getBean(ObservationRegistry.class);
376+
}
377+
catch (NoSuchBeanDefinitionException ex) {
378+
}
369379
}
370380

371381
@Override
372382
public void setServletContext(ServletContext servletContext) {
373383
this.servletContext = servletContext;
374384
}
375385

386+
FilterChainProxy.FilterChainDecorator getFilterChainDecorator() {
387+
if (this.observationRegistry.isNoop()) {
388+
return new FilterChainProxy.VirtualFilterChainDecorator();
389+
}
390+
return new ObservationFilterChainDecorator(this.observationRegistry);
391+
}
392+
376393
/**
377394
* Allows registering {@link RequestMatcher} instances that should be ignored by
378395
* Spring Security.

config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.Arrays;
2020
import java.util.List;
2121

22+
import io.micrometer.observation.ObservationRegistry;
23+
2224
import org.springframework.beans.factory.annotation.Autowired;
2325
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
2426
import org.springframework.context.ApplicationContext;
@@ -28,6 +30,7 @@
2830
import org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor;
2931
import org.springframework.security.config.web.server.ServerHttpSecurity;
3032
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
33+
import org.springframework.security.web.server.ObservationWebFilterChainDecorator;
3134
import org.springframework.security.web.server.SecurityWebFilterChain;
3235
import org.springframework.security.web.server.WebFilterChainProxy;
3336
import org.springframework.util.ClassUtils;
@@ -55,6 +58,8 @@ class WebFluxSecurityConfiguration {
5558

5659
private List<SecurityWebFilterChain> securityWebFilterChains;
5760

61+
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
62+
5863
@Autowired
5964
ApplicationContext context;
6065

@@ -63,10 +68,19 @@ void setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterCh
6368
this.securityWebFilterChains = securityWebFilterChains;
6469
}
6570

71+
@Autowired(required = false)
72+
void setObservationRegistry(ObservationRegistry observationRegistry) {
73+
this.observationRegistry = observationRegistry;
74+
}
75+
6676
@Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
6777
@Order(WEB_FILTER_CHAIN_FILTER_ORDER)
6878
WebFilterChainProxy springSecurityWebFilterChainFilter() {
69-
return new WebFilterChainProxy(getSecurityWebFilterChains());
79+
WebFilterChainProxy proxy = new WebFilterChainProxy(getSecurityWebFilterChains());
80+
if (!this.observationRegistry.isNoop()) {
81+
proxy.setFilterChainDecorator(new ObservationWebFilterChainDecorator(this.observationRegistry));
82+
}
83+
return proxy;
7084
}
7185

7286
@Bean(name = AbstractView.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)

config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.springframework.security.config.authentication.AuthenticationManagerFactoryBean;
5757
import org.springframework.security.web.DefaultSecurityFilterChain;
5858
import org.springframework.security.web.FilterChainProxy;
59+
import org.springframework.security.web.ObservationFilterChainDecorator;
5960
import org.springframework.security.web.PortResolverImpl;
6061
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
6162
import org.springframework.util.StringUtils;
@@ -363,6 +364,10 @@ static void registerFilterChainProxyIfNecessary(ParserContext pc, Object source)
363364
fcpBldr.getRawBeanDefinition().setSource(source);
364365
fcpBldr.addConstructorArgReference(BeanIds.FILTER_CHAINS);
365366
fcpBldr.addPropertyValue("filterChainValidator", new RootBeanDefinition(DefaultFilterChainValidator.class));
367+
BeanDefinition filterChainDecorator = BeanDefinitionBuilder
368+
.rootBeanDefinition(FilterChainDecoratorFactory.class)
369+
.addPropertyValue("observationRegistry", getObservationRegistry(element)).getBeanDefinition();
370+
fcpBldr.addPropertyValue("filterChainDecorator", filterChainDecorator);
366371
BeanDefinition fcpBean = fcpBldr.getBeanDefinition();
367372
pc.registerBeanComponent(new BeanComponentDefinition(fcpBean, BeanIds.FILTER_CHAIN_PROXY));
368373
registry.registerAlias(BeanIds.FILTER_CHAIN_PROXY, BeanIds.SPRING_SECURITY_FILTER_CHAIN);
@@ -509,4 +514,28 @@ public Class<?> getObjectType() {
509514

510515
}
511516

517+
public static final class FilterChainDecoratorFactory
518+
implements FactoryBean<FilterChainProxy.FilterChainDecorator> {
519+
520+
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
521+
522+
@Override
523+
public FilterChainProxy.FilterChainDecorator getObject() throws Exception {
524+
if (this.observationRegistry.isNoop()) {
525+
return new FilterChainProxy.VirtualFilterChainDecorator();
526+
}
527+
return new ObservationFilterChainDecorator(this.observationRegistry);
528+
}
529+
530+
@Override
531+
public Class<?> getObjectType() {
532+
return FilterChainProxy.FilterChainDecorator.class;
533+
}
534+
535+
public void setObservationRegistry(ObservationRegistry registry) {
536+
this.observationRegistry = registry;
537+
}
538+
539+
}
540+
512541
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web.configurers;
18+
19+
import java.util.Iterator;
20+
21+
import io.micrometer.observation.Observation;
22+
import io.micrometer.observation.ObservationHandler;
23+
import io.micrometer.observation.ObservationRegistry;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.mockito.ArgumentCaptor;
27+
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
32+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
33+
import org.springframework.security.config.test.SpringTestContext;
34+
import org.springframework.security.config.test.SpringTestContextExtension;
35+
import org.springframework.security.core.userdetails.User;
36+
import org.springframework.security.core.userdetails.UserDetailsService;
37+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
38+
import org.springframework.security.web.SecurityFilterChain;
39+
import org.springframework.test.web.servlet.MockMvc;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
import static org.mockito.ArgumentMatchers.any;
43+
import static org.mockito.BDDMockito.given;
44+
import static org.mockito.Mockito.mock;
45+
import static org.mockito.Mockito.times;
46+
import static org.mockito.Mockito.verify;
47+
import static org.springframework.security.config.Customizer.withDefaults;
48+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
49+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
50+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
51+
52+
/**
53+
* @author Josh Cummings
54+
*
55+
*/
56+
@ExtendWith(SpringTestContextExtension.class)
57+
public class HttpSecurityObservationTests {
58+
59+
@Autowired
60+
MockMvc mvc;
61+
62+
public final SpringTestContext spring = new SpringTestContext(this);
63+
64+
@Test
65+
public void getWhenUsingObservationRegistryThenObservesRequest() throws Exception {
66+
this.spring.register(ObservationRegistryConfig.class).autowire();
67+
// @formatter:off
68+
this.mvc.perform(get("/").with(httpBasic("user", "password")))
69+
.andExpect(status().isNotFound());
70+
// @formatter:on
71+
ObservationHandler<Observation.Context> handler = this.spring.getContext().getBean(ObservationHandler.class);
72+
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
73+
verify(handler, times(5)).onStart(captor.capture());
74+
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
75+
assertThat(contexts.next().getContextualName()).isEqualTo("spring.security.http.chains.before");
76+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications");
77+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authorizations");
78+
assertThat(contexts.next().getName()).isEqualTo("spring.security.http.secured.requests");
79+
assertThat(contexts.next().getContextualName()).isEqualTo("spring.security.http.chains.after");
80+
}
81+
82+
@EnableWebSecurity
83+
@Configuration
84+
static class ObservationRegistryConfig {
85+
86+
private ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
87+
88+
@Bean
89+
SecurityFilterChain app(HttpSecurity http) throws Exception {
90+
http.httpBasic(withDefaults()).authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
91+
return http.build();
92+
}
93+
94+
@Bean
95+
UserDetailsService userDetailsService() {
96+
return new InMemoryUserDetailsManager(
97+
User.withDefaultPasswordEncoder().username("user").password("password").authorities("app").build());
98+
}
99+
100+
@Bean
101+
ObservationHandler<Observation.Context> observationHandler() {
102+
return this.handler;
103+
}
104+
105+
@Bean
106+
ObservationRegistry observationRegistry() {
107+
given(this.handler.supportsContext(any())).willReturn(true);
108+
ObservationRegistry registry = ObservationRegistry.create();
109+
registry.observationConfig().observationHandler(this.handler);
110+
return registry;
111+
}
112+
113+
}
114+
115+
}

config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@
1616

1717
package org.springframework.security.config.http;
1818

19+
import java.util.Iterator;
20+
21+
import io.micrometer.observation.Observation;
22+
import io.micrometer.observation.ObservationHandler;
23+
import io.micrometer.observation.ObservationRegistry;
1924
import jakarta.servlet.http.HttpServletRequest;
2025
import jakarta.servlet.http.HttpServletResponse;
2126
import jakarta.servlet.http.HttpServletResponseWrapper;
2227
import org.apache.http.HttpStatus;
2328
import org.junit.jupiter.api.Test;
2429
import org.junit.jupiter.api.extension.ExtendWith;
30+
import org.mockito.ArgumentCaptor;
2531

32+
import org.springframework.beans.factory.FactoryBean;
2633
import org.springframework.beans.factory.annotation.Autowired;
2734
import org.springframework.mock.web.MockHttpServletRequest;
2835
import org.springframework.mock.web.MockHttpServletResponse;
@@ -36,7 +43,10 @@
3643
import static org.assertj.core.api.Assertions.assertThat;
3744
import static org.mockito.ArgumentMatchers.any;
3845
import static org.mockito.BDDMockito.given;
46+
import static org.mockito.Mockito.mock;
47+
import static org.mockito.Mockito.times;
3948
import static org.mockito.Mockito.verify;
49+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
4050
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
4151
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
4252
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -101,6 +111,24 @@ public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter()
101111
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login");
102112
}
103113

114+
@Test
115+
public void getWhenUsingObservationRegistryThenObservesRequest() throws Exception {
116+
this.spring.configLocations(this.xml("WithObservationRegistry")).autowire();
117+
// @formatter:off
118+
this.mvc.perform(get("/").with(httpBasic("user", "password")))
119+
.andExpect(status().isNotFound());
120+
// @formatter:on
121+
ObservationHandler<Observation.Context> handler = this.spring.getContext().getBean(ObservationHandler.class);
122+
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
123+
verify(handler, times(5)).onStart(captor.capture());
124+
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
125+
assertThat(contexts.next().getContextualName()).isEqualTo("spring.security.http.chains.before");
126+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications");
127+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authorizations");
128+
assertThat(contexts.next().getName()).isEqualTo("spring.security.http.secured.requests");
129+
assertThat(contexts.next().getContextualName()).isEqualTo("spring.security.http.chains.after");
130+
}
131+
104132
private String xml(String configName) {
105133
return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
106134
}
@@ -133,4 +161,27 @@ public String encodeRedirectUrl(String url) {
133161

134162
}
135163

164+
public static final class MockObservationRegistry implements FactoryBean<ObservationRegistry> {
165+
166+
private ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
167+
168+
@Override
169+
public ObservationRegistry getObject() {
170+
ObservationRegistry registry = ObservationRegistry.create();
171+
registry.observationConfig().observationHandler(this.handler);
172+
given(this.handler.supportsContext(any())).willReturn(true);
173+
return registry;
174+
}
175+
176+
@Override
177+
public Class<?> getObjectType() {
178+
return ObservationRegistry.class;
179+
}
180+
181+
public void setHandler(ObservationHandler<Observation.Context> handler) {
182+
this.handler = handler;
183+
}
184+
185+
}
186+
136187
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2018 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xmlns="http://www.springframework.org/schema/security"
21+
xsi:schemaLocation="
22+
http://www.springframework.org/schema/security
23+
https://www.springframework.org/schema/security/spring-security.xsd
24+
http://www.springframework.org/schema/beans
25+
https://www.springframework.org/schema/beans/spring-beans.xsd">
26+
27+
<http auto-config="true" observation-registry-ref="ref" use-authorization-manager="true">
28+
<intercept-url pattern="/**" access="hasRole('USER')"/>
29+
</http>
30+
31+
<b:bean name="handler" class="org.mockito.Mockito" factory-method="mock">
32+
<b:constructor-arg value="io.micrometer.observation.ObservationHandler"/>
33+
</b:bean>
34+
35+
<b:bean name="ref" class="org.springframework.security.config.http.HttpConfigTests.MockObservationRegistry">
36+
<b:property name="handler" ref="handler"/>
37+
</b:bean>
38+
39+
<b:import resource="userservice.xml"/>
40+
</b:beans>

0 commit comments

Comments
 (0)