diff --git a/docs/modules/ROOT/pages/servlet/architecture.adoc b/docs/modules/ROOT/pages/servlet/architecture.adoc index bba7f2357cb..280f3764461 100644 --- a/docs/modules/ROOT/pages/servlet/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/architecture.adoc @@ -51,28 +51,9 @@ image::{figures}/delegatingfilterproxy.png[] The following listing shows pseudo code of `DelegatingFilterProxy`: .`DelegatingFilterProxy` Pseudo Code -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { - Filter delegate = getFilterBean(someBeanName); // <1> - delegate.doFilter(request, response); // <2> -} ----- -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { - val delegate: Filter = getFilterBean(someBeanName) // <1> - delegate.doFilter(request, response) // <2> -} ----- -====== +include-code::./SampleDelegatingFilterProxy[tag=dofilter,indent=0] + <1> Lazily get Filter that was registered as a Spring Bean. For the example in <> `delegate` is an instance of __Bean Filter~0~__. <2> Delegate work to the Spring Bean. @@ -155,58 +136,7 @@ However, there are times that it is beneficial to know the ordering, if you want These security filters are most often declared using an javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[`HttpSecurity`] instance. To exemplify the above paragraph, let's consider the following security configuration: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(Customizer.withDefaults()) - .httpBasic(Customizer.withDefaults()) - .formLogin(Customizer.withDefaults()) - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ); - - return http.build(); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.web.servlet.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun filterChain(http: HttpSecurity): SecurityFilterChain { - http { - csrf { } - httpBasic { } - formLogin { } - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - } - return http.build() - } - -} ----- -====== +include-code::./SecurityConfig[tag=snippet,indent=0] The above configuration will result in the following `Filter` ordering: @@ -233,7 +163,7 @@ If you want to see the list of filters invoked for a particular request, you can === Printing the Security Filters Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request. -For example, you want to make sure that the <> is in the list of the security filters. +For example, you want to make sure that the <> is in the list of the security filters. The list of filters is printed at DEBUG level on the application startup, so you can see something like the following on the console output for example: @@ -248,7 +178,7 @@ But that is not all, you can also configure your application to print the invoca That is helpful to see if the filter you have added is invoked for a particular request or to check where an exception is coming from. To do that, you can configure your application to <>. -[[adding-custom-filter]] +[[adding-filters-to-chain]] === Adding Filters to the Filter Chain Most of the time, the default <> are enough to provide security to your application. @@ -260,6 +190,7 @@ javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity * `#addFilterAfter(Filter, Class)` adds your filter after another filter * `#addFilterAt(Filter, Class)` replaces another filter with your filter +[[adding-custom-filter]] ==== Adding a Custom Filter If you are creating a filter of your own, you will need to determine its location in the filter chain. @@ -298,39 +229,7 @@ For example, let's say that you want to add a `Filter` that gets a tenant id hea First, let's create the `Filter`: -[source,java] ----- -import java.io.IOException; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.springframework.security.access.AccessDeniedException; - -public class TenantFilter implements Filter { - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - - String tenantId = request.getHeader("X-Tenant-Id"); <1> - boolean hasAccess = isUserAllowed(tenantId); <2> - if (hasAccess) { - filterChain.doFilter(request, response); <3> - return; - } - throw new AccessDeniedException("Access denied"); <4> - } - -} - ----- +include-code::./TenantFilter[tag=snippet,indent=0] The sample code above does the following: @@ -349,34 +248,7 @@ The previous description already gives us a clue on where to add the filter, sin Based on the rule of thumb, add it after xref:servlet/authentication/anonymous.adoc[ `AnonymousAuthenticationFilter`], the last authentication filter in the chain, like so: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - // ... - .addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); <1> - return http.build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun filterChain(http: HttpSecurity): SecurityFilterChain { - http - // ... - .addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) <1> - return http.build() -} ----- -====== +include-code::./SecurityConfig[tag=snippet,indent=0] <1> Use `HttpSecurity#addFilterAfter` to add the `TenantFilter` after the `AnonymousAuthenticationFilter`. @@ -405,127 +277,23 @@ public FilterRegistrationBean tenantFilterRegistration(TenantFilte This makes so that `HttpSecurity` is the only one adding it. +[[customizing-filter]] ==== Customizing a Spring Security Filter Generally, you can use a filter's DSL method to configure Spring Security's filters. For example, the simplest way to add `BasicAuthenticationFilter` is by asking the DSL to do it: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .httpBasic(Customizer.withDefaults()) - // ... - - return http.build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun filterChain(http: HttpSecurity): SecurityFilterChain { - http { - httpBasic { } - // ... - } - - return http.build() -} ----- -====== - +include-code::./CustomizingFilterTests[tag=basic-default,indent=0] However, in the event that you want to construct a Spring Security filter yourself, you specify it in the DSL using `addFilterAt` like so: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - BasicAuthenticationFilter basic = new BasicAuthenticationFilter(); - // ... configure - - http - // ... - .addFilterAt(basic, BasicAuthenticationFilter.class); - - return http.build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun filterChain(http: HttpSecurity): SecurityFilterChain { - val basic = BasicAuthenticationFilter() - // ... configure - - http - // ... - .addFilterAt(basic, BasicAuthenticationFilter::class.java) - - return http.build() -} ----- -====== +include-code::./CustomizingFilterTests[tag=custom-filter,indent=0] Note that if that filter has already been added, then Spring Security will throw an exception. For example, calling xref:servlet/authentication/passwords/basic.adoc[ `HttpSecurity#httpBasic`] adds a `BasicAuthenticationFilter` for you. So, the following arrangement fails since there are two calls that are both trying to add `BasicAuthenticationFilter`: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - BasicAuthenticationFilter basic = new BasicAuthenticationFilter(); - // ... configure - - http - .httpBasic(Customizer.withDefaults()) - // ... on no! BasicAuthenticationFilter is added twice! - .addFilterAt(basic, BasicAuthenticationFilter.class); - - return http.build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun filterChain(http: HttpSecurity): SecurityFilterChain { - val basic = BasicAuthenticationFilter() - // ... configure - - http { - httpBasic { } - } - - // ... on no! BasicAuthenticationFilter is added twice! - http.addFilterAt(basic, BasicAuthenticationFilter::class.java) - - return http.build() -} ----- -====== +include-code::./CustomizingFilterTests[tag=incorrect,indent=0] In this case, remove the call to `httpBasic` since you are constructing `BasicAuthenticationFilter` yourself. @@ -533,10 +301,7 @@ In this case, remove the call to `httpBasic` since you are constructing `BasicAu ==== In the event that you are unable to reconfigure `HttpSecurity` to not add a certain filter, you can typically disable the Spring Security filter by calling its DSL's `disable` method like so: -[source,java] ----- -.httpBasic((basic) -> basic.disable()) ----- +include-code::./CustomizingFilterTests[tag=disable,indent=0] ==== [[servlet-exceptiontranslationfilter]] @@ -618,53 +383,7 @@ Or you may want to shut off this feature since you always want to redirect the u To do that, you can use the javadoc:org.springframework.security.web.savedrequest.NullRequestCache[NullRequestCache] implementation. .Prevent the Request From Being Saved -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { - RequestCache nullRequestCache = new NullRequestCache(); - http - // ... - .requestCache((cache) -> cache - .requestCache(nullRequestCache) - ); - return http.build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -open fun springSecurity(http: HttpSecurity): SecurityFilterChain { - val nullRequestCache = NullRequestCache() - http { - requestCache { - requestCache = nullRequestCache - } - } - return http.build() -} ----- - -XML:: -+ -[source,xml,role="secondary"] ----- - - - - - - ----- -====== - +include-code::./SecurityConfig[tag=snippet,indent=0] [[requestcacheawarefilter]] === RequestCacheAwareFilter diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/CustomFilterTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/CustomFilterTests.java new file mode 100644 index 00000000000..b24b1274db9 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/CustomFilterTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.addingcustomfilter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + CustomFilterTests.UserDetailsConfig.class, + CustomFilterTests.ApiController.class, + SecurityConfig.class }) +@WebAppConfiguration +public class CustomFilterTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @BeforeEach + void setup() { + this.mvc = MockMvcBuilders.webAppContextSetup(this.context) + .defaultRequest(get("/api").with(user("user"))) + .apply(springSecurity()) + .build(); + } + + @Test + void tenantFilterWhenHeaderMissingThenAccessDenied() { + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.mvc.perform(get("/api")).andReturn()); + } + + @Test + void tenantFilterWhenHeaderPresentThenContinuesFilterChain() throws Exception { + this.mvc.perform(get("/api").header("X-Tenant-Id", "some-tenant-id")) + .andExpect(status().isOk()) + .andExpect(authenticated().withUsername("user")); + } + + @Configuration + static class UserDetailsConfig { + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } + } + + @RestController + static class ApiController { + + @GetMapping("/api") + String api() { + return "ok"; + } + + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/SecurityConfig.java b/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/SecurityConfig.java new file mode 100644 index 00000000000..587d9c651b1 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/SecurityConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.addingcustomfilter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.test.context.ContextConfiguration; + +@Configuration +@ContextConfiguration(classes = { SecurityConfig.class }) +@EnableWebSecurity +public class SecurityConfig { + + // tag::snippet[] + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // ... + .addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); // <1> + return http.build(); + } + // end::snippet[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/TenantFilter.java b/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/TenantFilter.java new file mode 100644 index 00000000000..d92172bf6c8 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/addingcustomfilter/TenantFilter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.addingcustomfilter; + +// tag::snippet[] +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.security.access.AccessDeniedException; + +public class TenantFilter implements Filter { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + String tenantId = request.getHeader("X-Tenant-Id"); // <1> + boolean hasAccess = isUserAllowed(tenantId); // <2> + if (hasAccess) { + filterChain.doFilter(request, response); // <3> + return; + } + throw new AccessDeniedException("Access denied"); // <4> + } + + private boolean isUserAllowed(String tenantId) { + return "some-tenant-id".equals(tenantId); + } + +} +// end::snippet[] diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/customizingfilter/CustomizingFilterTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/customizingfilter/CustomizingFilterTests.java new file mode 100644 index 00000000000..c53800e36bf --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/customizingfilter/CustomizingFilterTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.customizingfilter; + +import java.io.IOException; +import java.util.List; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +/** + * Tests for customizing security filters. + * + */ +@ExtendWith(SpringTestContextExtension.class) +public class CustomizingFilterTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + MockMvc mvc; + + @Autowired + FilterChainProxy filterChainProxy; + + @Test + public void filterChainWhenBasicDefaultThenBasicAuthenticationFilterPresent() { + this.spring.register(SecurityConfigBasicDefault.class).autowire(); + List filters = this.filterChainProxy.getFilters("/"); + assertThat(filters).extracting("class").contains(BasicAuthenticationFilter.class); + } + + @Test + public void filterChainWhenCustomFilterThenCustomFilterPresent() { + this.spring.register(SecurityConfigCustom.class).autowire(); + List filters = this.filterChainProxy.getFilters("/"); + assertThat(filters).extracting("class").contains(SecurityConfigCustom.MyBasicAuthenticationFilter.class); + assertThat(filters).extracting("class").doesNotContain(BasicAuthenticationFilter.class); + } + + @Test + public void requestWhenDisableThenNoWwwAuthenticateHeader() throws Exception { + this.spring.register(SecurityConfigDisable.class).autowire(); + this.mvc.perform(get("/")).andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE)); + } + + @Test + public void filterChainWhenIncorrectThenBothFiltersPresent() { + this.spring.register(SecurityConfigIncorrect.class).autowire(); + List filters = this.filterChainProxy.getFilters("/"); + assertThat(filters).extracting("class").contains(BasicAuthenticationFilter.class); + assertThat(filters).extracting("class").contains(SecurityConfigIncorrect.MyBasicAuthenticationFilter.class); + } + + @Configuration + @EnableWebSecurity + static class SecurityConfigBasicDefault { + + // tag::basic-default[] + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .httpBasic(Customizer.withDefaults()); + // ... + + return http.build(); + } + // end::basic-default[] + + } + + @Configuration + @EnableWebSecurity + static class SecurityConfigCustom { + + // tag::custom-filter[] + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + MyBasicAuthenticationFilter basic = new MyBasicAuthenticationFilter(); + // ... configure + + http + // ... + .addFilterAt(basic, BasicAuthenticationFilter.class); + + return http.build(); + } + // end::custom-filter[] + + static class MyBasicAuthenticationFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + } + + } + + } + + @Configuration + @EnableWebSecurity + static class SecurityConfigDisable { + + // tag::disable[] + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .httpBasic((basic) -> basic.disable()); + // ... + + return http.build(); + } + // end::disable[] + + } + + @Configuration + @EnableWebSecurity + static class SecurityConfigIncorrect { + + // tag::incorrect[] + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + MyBasicAuthenticationFilter basic = new MyBasicAuthenticationFilter(); + // ... configure + + http + .httpBasic(Customizer.withDefaults()) + // ... on no! BasicAuthenticationFilter is added twice! + .addFilterAt(basic, BasicAuthenticationFilter.class); + + return http.build(); + } + // end::incorrect[] + + static class MyBasicAuthenticationFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + } + + } + + } + + @Configuration + static class UserDetailsConfig { + + @Bean + InMemoryUserDetailsManager userDetailsManager() { + return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build()); + } + + } + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.java b/docs/src/test/java/org/springframework/security/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.java new file mode 100644 index 00000000000..95604bec0df --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.java @@ -0,0 +1,40 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.requestcachepreventsavedrequest; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.savedrequest.NullRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; + +public class SecurityConfig { + + // tag::snippet[] + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + RequestCache nullRequestCache = new NullRequestCache(); + http + // ... + .requestCache((cache) -> cache + .requestCache(nullRequestCache) + ); + return http.build(); + } + // end::snippet[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxy.java b/docs/src/test/java/org/springframework/security/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxy.java new file mode 100644 index 00000000000..ce3bce01083 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxy.java @@ -0,0 +1,53 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.servletdelegatingfilterproxy; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +/** + * A very simple implementation of a DelegatingFilterProxy. + */ +public class SampleDelegatingFilterProxy extends GenericFilterBean { + + private StaticWebApplicationContext wac; + private final String someBeanName; + + public SampleDelegatingFilterProxy(String someBeanName, StaticWebApplicationContext webApplicationContext) { + this.wac = webApplicationContext; + this.someBeanName = someBeanName; + } + + // tag::dofilter[] + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + Filter delegate = getFilterBean(someBeanName); // <1> + delegate.doFilter(request, response, chain); // <2> + } + // end::dofilter[] + + private Filter getFilterBean(String someBeanName) { + return this.wac.getBean(someBeanName, Filter.class); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxyTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxyTests.java new file mode 100644 index 00000000000..9ac8e619256 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxyTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.servletdelegatingfilterproxy; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.servlet.MockServletContext; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SampleDelegatingFilterProxyTests { + + @Test + void testFilter() throws ServletException, IOException { + ServletContext sc = new MockServletContext(); + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.registerSingleton("targetFilter", MockFilter.class); + wac.setServletContext(sc); + wac.refresh(); + sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); + + MockFilter targetFilter = (MockFilter) wac.getBean("targetFilter"); + MockFilterConfig proxyConfig = new MockFilterConfig(sc); + proxyConfig.addInitParameter("targetBeanName", "targetFilter"); + SampleDelegatingFilterProxy filterProxy = new SampleDelegatingFilterProxy("targetFilter", wac); + filterProxy.init(proxyConfig); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + filterProxy.doFilter(request, response, null); + + assertThat(targetFilter.filterConfig).isNull(); + assertThat(request.getAttribute("called")).isEqualTo(Boolean.TRUE); + + filterProxy.destroy(); + assertThat(targetFilter.filterConfig).isNull(); + } + + private static class MockFilter implements Filter { + + private FilterConfig filterConfig; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + this.filterConfig = filterConfig; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws java.io.IOException, ServletException { + request.setAttribute("called", Boolean.TRUE); + } + + @Override + public void destroy() { + this.filterConfig = null; + } + } + + private static class MockFilterConfig implements FilterConfig { + private final ServletContext servletContext; + + private final String filterName; + + private final Map initParameters = new LinkedHashMap<>(); + + public MockFilterConfig(ServletContext servletContext) { + this.servletContext = servletContext; + this.filterName = ""; + } + + @Override + public String getFilterName() { + return this.filterName; + } + + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + @Override + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/servletsecurityfilters/SampleSecurityConfigTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/servletsecurityfilters/SampleSecurityConfigTests.java new file mode 100644 index 00000000000..12be470d956 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/servletsecurityfilters/SampleSecurityConfigTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.servletsecurityfilters; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { SampleSecurityConfigTests.UserDetailsConfig.class, SecurityConfig.class }) +@WebAppConfiguration +public class SampleSecurityConfigTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @BeforeEach + public void setup() { + this.mvc = MockMvcBuilders.webAppContextSetup(this.context) + .defaultRequest(get("/api").with(user("user"))) + .defaultRequest(post("/api").with(csrf())) + .apply(springSecurity()) + .build(); + } + + @Test + void testGet() throws Exception { + this.mvc.perform(get("/api") + .with(httpBasic("user", "password"))) + // Security check was successful + .andExpect(status().isNotFound()) + .andExpect(authenticated().withUsername("user")); + } + + @Test + void testUnauthenticated() throws Exception { + this.mvc.perform(get("/api")) + // Security check was successful + .andExpect(status().isUnauthorized()); + } + + @Test + void testCsrf() throws Exception { + this.mvc.perform(post("/api") + .with(csrf()) + .with(httpBasic("user", "password")) + ).andExpect(status().isNotFound()); + } + + @Configuration + static class UserDetailsConfig { + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .build(); + return new InMemoryUserDetailsManager(user); + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/servletsecurityfilters/SecurityConfig.java b/docs/src/test/java/org/springframework/security/docs/servlet/servletsecurityfilters/SecurityConfig.java new file mode 100644 index 00000000000..6f44d888f0d --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/servletsecurityfilters/SecurityConfig.java @@ -0,0 +1,45 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.servletsecurityfilters; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +// tag::snippet[] +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(Customizer.withDefaults()) + .httpBasic(Customizer.withDefaults()) + .formLogin(Customizer.withDefaults()) + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ); + + return http.build(); + } + +} +// end::snippet[] diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/CustomFilterTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/CustomFilterTests.kt new file mode 100644 index 00000000000..bd7c8241db4 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/CustomFilterTests.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.addingcustomfilter + +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.context.web.WebAppConfiguration +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.context.WebApplicationContext +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@ExtendWith(SpringExtension::class) +@ContextConfiguration( + classes = [ + CustomFilterTests.UserDetailsConfig::class, + CustomFilterTests.ApiController::class, + SecurityConfig::class + ] +) +@WebAppConfiguration +class CustomFilterTests { + + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mvc: MockMvc + + @BeforeEach + fun setup() { + this.mvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(springSecurity()) + .build(); + } + + @Test + fun tenantFilterWhenHeaderMissingThenAccessDenied() { + assertThatExceptionOfType(Exception::class.java) + .isThrownBy { this.mvc.perform(get("/api").with(user("user"))).andReturn() } + } + + @Test + fun tenantFilterWhenHeaderPresentThenContinuesFilterChain() { + this.mvc.perform(get("/api") + .with(user("user")) + .header("X-Tenant-Id", "some-tenant-id")) + .andExpect(status().isOk) + .andExpect(authenticated().withUsername("user")) + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): UserDetailsService { + @Suppress("DEPRECATION") + val user: UserDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(user) + } + } + + @RestController + class ApiController { + + @GetMapping("/api") + fun api(): String { + return "ok" + } + + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/SecurityConfig.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/SecurityConfig.kt new file mode 100644 index 00000000000..30bd94f7407 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/SecurityConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.addingcustomfilter + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter + +@Configuration +@EnableWebSecurity +open class SecurityConfig { + + // tag::snippet[] + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http + // ... + .addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) // <1> + return http.build() + } + // end::snippet[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/TenantFilter.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/TenantFilter.kt new file mode 100644 index 00000000000..fa76d65fb7a --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/addingcustomfilter/TenantFilter.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.addingcustomfilter + +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletException +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.access.AccessDeniedException +import java.io.IOException + +// tag::snippet[] +class TenantFilter : Filter { + + @Throws(IOException::class, ServletException::class) + override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) { + val request = servletRequest as HttpServletRequest + val response = servletResponse as HttpServletResponse + + val tenantId = request.getHeader("X-Tenant-Id") // <1> + val hasAccess = isUserAllowed(tenantId) // <2> + if (hasAccess) { + filterChain.doFilter(request, response) // <3> + return + } + throw AccessDeniedException("Access denied") // <4> + } + + private fun isUserAllowed(tenantId: String?): Boolean { + return "some-tenant-id" == tenantId + } + +} +// end::snippet[] diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/customizingfilter/CustomizingFilterTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/customizingfilter/CustomizingFilterTests.kt new file mode 100644 index 00000000000..17c6a3b13fb --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/customizingfilter/CustomizingFilterTests.kt @@ -0,0 +1,167 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.customizingfilter + +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter +import org.springframework.test.web.servlet.MockMvc + +/** + * Tests for customizing security filters. + * + */ +@ExtendWith(SpringTestContextExtension::class) +class CustomizingFilterTests { + + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mvc: MockMvc + + @Autowired + lateinit var filterChainProxy: FilterChainProxy + + @Test + fun `filter chain when basic default then BasicAuthenticationFilter present`() { + spring.register(SecurityConfigBasicDefault::class.java).autowire() + val filters = filterChainProxy.getFilters("/") + assertThat(filters).extracting("class").contains(BasicAuthenticationFilter::class.java) + } + + @Test + fun `filter chain when custom filter then custom filter present`() { + spring.register(SecurityConfigCustom::class.java).autowire() + val filters = filterChainProxy.getFilters("/") + assertThat(filters).extracting("class").contains(SecurityConfigCustom.MyBasicAuthenticationFilter::class.java) + assertThat(filters).extracting("class").doesNotContain(BasicAuthenticationFilter::class.java) + } + + @Test + fun `filter chain when incorrect then both filters present`() { + spring.register(SecurityConfigIncorrect::class.java).autowire() + val filters = filterChainProxy.getFilters("/") + assertThat(filters).extracting("class").contains(BasicAuthenticationFilter::class.java) + assertThat(filters).extracting("class").contains(SecurityConfigIncorrect.MyBasicAuthenticationFilter::class.java) + } + + @Configuration + @EnableWebSecurity + open class SecurityConfigBasicDefault { + + // tag::basic-default[] + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + httpBasic { } + // ... + } + return http.build() + } + // end::basic-default[] + + } + + @Configuration + @EnableWebSecurity + open class SecurityConfigCustom { + + // tag::custom-filter[] + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + val basic = MyBasicAuthenticationFilter() + // ... configure + + http + // ... + .addFilterAt(basic, BasicAuthenticationFilter::class.java) + + return http.build() + } + // end::custom-filter[] + + class MyBasicAuthenticationFilter : Filter { + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + chain.doFilter(request, response) + } + } + + } + + @Configuration @EnableWebSecurity + open class SecurityConfigDisable { + + // tag::disable[] + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + httpBasic { + disable() + } + // ... + } + return http.build() + } + // end::disable[] + } + + @Configuration + @EnableWebSecurity + open class SecurityConfigIncorrect { + + // tag::incorrect[] + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + val basic = MyBasicAuthenticationFilter() + // ... configure + + http { + httpBasic { } + } + + // ... on no! BasicAuthenticationFilter is added twice! + http.addFilterAt(basic, BasicAuthenticationFilter::class.java) + + return http.build() + } + // end::incorrect[] + + class MyBasicAuthenticationFilter : Filter { + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + chain.doFilter(request, response) + } + } + + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.kt new file mode 100644 index 00000000000..ea330dd7361 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.requestcachepreventsavedrequest + +import org.springframework.context.annotation.Bean +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.savedrequest.NullRequestCache + +open class SecurityConfig { + + // tag::snippet[] + @Bean + open fun springSecurity(http: HttpSecurity): SecurityFilterChain { + val nullRequestCache = NullRequestCache() + http { + requestCache { + requestCache = nullRequestCache + } + } + return http.build() + } + // end::snippet[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxy.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxy.kt new file mode 100644 index 00000000000..8ff485673aa --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxy.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.servletdelegatingfilterproxy; + +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletException +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + + +/** + * A very simple implementation of a DelegatingFilterProxy. + */ +class SampleDelegatingFilterProxy( + private val someBeanName: String, + private var wac: StaticWebApplicationContext +) : GenericFilterBean() { + + // tag::dofilter[] + @Throws(IOException::class, ServletException::class) + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain?) { + val delegate: Filter = getFilterBean(someBeanName) // <1> + delegate.doFilter(request, response, chain) // <2> + } + // end::dofilter[] + + private fun getFilterBean(someBeanName: String): Filter { + return wac.getBean(someBeanName, Filter::class.java) + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxyTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxyTests.kt new file mode 100644 index 00000000000..ec556653290 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletdelegatingfilterproxy/SampleDelegatingFilterProxyTests.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.servletdelegatingfilterproxy + +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.FilterConfig +import jakarta.servlet.ServletContext +import jakarta.servlet.ServletException +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.security.web.servlet.MockServletContext +import org.springframework.util.Assert +import org.springframework.web.context.WebApplicationContext +import org.springframework.web.context.support.StaticWebApplicationContext +import java.io.IOException +import org.assertj.core.api.Assertions.assertThat +import java.util.Enumeration +import java.util.Collections +import kotlin.collections.LinkedHashMap + +class SampleDelegatingFilterProxyTests { + + @Test + @Throws(ServletException::class, IOException::class) + fun testFilter() { + val sc: ServletContext = MockServletContext() + val wac = StaticWebApplicationContext() + wac.registerSingleton("targetFilter", MockFilter::class.java) + wac.setServletContext(sc) + wac.refresh() + sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac) + + val targetFilter = wac.getBean("targetFilter") as MockFilter + val proxyConfig = MockFilterConfig(sc) + proxyConfig.addInitParameter("targetBeanName", "targetFilter") + val filterProxy = SampleDelegatingFilterProxy("targetFilter", wac) + filterProxy.init(proxyConfig) + + val request = MockHttpServletRequest() + val response = MockHttpServletResponse() + filterProxy.doFilter(request, response, null) + + assertThat(targetFilter.filterConfig).isNull() + assertThat(request.getAttribute("called")).isEqualTo(true) + + filterProxy.destroy() + assertThat(targetFilter.filterConfig).isNull() + } + + private class MockFilter : Filter { + var filterConfig: FilterConfig? = null + + @Throws(ServletException::class) + override fun init(filterConfig: FilterConfig) { + this.filterConfig = filterConfig + } + + @Throws(IOException::class, ServletException::class) + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain?) { + request.setAttribute("called", true) + } + + override fun destroy() { + filterConfig = null + } + } + + private class MockFilterConfig( + private val servletContext: ServletContext + ) : FilterConfig { + private val filterName: String = "" + private val initParameters = LinkedHashMap() + + override fun getFilterName(): String = filterName + + override fun getServletContext(): ServletContext = servletContext + + fun addInitParameter(name: String, value: String) { + Assert.notNull(name, "Parameter name must not be null") + initParameters[name] = value + } + + override fun getInitParameter(name: String): String? { + Assert.notNull(name, "Parameter name must not be null") + return initParameters[name] + } + + override fun getInitParameterNames(): Enumeration = + Collections.enumeration(initParameters.keys) + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletsecurityfilters/SecurityConfig.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletsecurityfilters/SecurityConfig.kt new file mode 100644 index 00000000000..8fe15ba7c11 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/servletsecurityfilters/SecurityConfig.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.servletsecurityfilters + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain + +// tag::snippet[] +@Configuration +@EnableWebSecurity +open class SecurityConfig { + + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + csrf { } + httpBasic { } + formLogin { } + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() + } + +} +// end::snippet[] diff --git a/docs/src/test/resources/org/springframework/security/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.xml b/docs/src/test/resources/org/springframework/security/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.xml new file mode 100644 index 00000000000..dea32c8b7f0 --- /dev/null +++ b/docs/src/test/resources/org/springframework/security/docs/servlet/requestcachepreventsavedrequest/SecurityConfig.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + +