Polish Filter Chain Documentation

Closes gh-15893
This commit is contained in:
Josh Cummings 2024-10-24 17:09:21 -06:00
parent cf03f2fed9
commit a36756929b
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5

View File

@ -164,11 +164,13 @@ In fact, a `SecurityFilterChain` might have zero security `Filter` instances if
== Security Filters
The Security Filters are inserted into the <<servlet-filterchainproxy>> with the <<servlet-securityfilterchain>> API.
Those filters can be used for a number of different purposes, like xref:servlet/authentication/index.adoc[authentication], xref:servlet/authorization/index.adoc[authorization], xref:servlet/exploits/index.adoc[exploit protection], and more.
Those filters can be used for a number of different purposes, like
xref:servlet/exploits/index.adoc[exploit protection],xref:servlet/authentication/index.adoc[authentication], xref:servlet/authorization/index.adoc[authorization], and more.
The filters are executed in a specific order to guarantee that they are invoked at the right time, for example, the `Filter` that performs authentication should be invoked before the `Filter` that performs authorization.
It is typically not necessary to know the ordering of Spring Security's ``Filter``s.
However, there are times that it is beneficial to know the ordering, if you want to know them, you can check the {gh-url}/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java[`FilterOrderRegistration` code].
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]
@ -185,11 +187,12 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
);
return http.build();
}
@ -210,11 +213,11 @@ class SecurityConfig {
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
httpBasic { }
formLogin { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic { }
formLogin { }
}
return http.build()
}
@ -235,8 +238,8 @@ The above configuration will result in the following `Filter` ordering:
|====
1. First, the `CsrfFilter` is invoked to protect against xref:servlet/exploits/csrf.adoc[CSRF attacks].
2. Second, the authentication filters are invoked to authenticate the request.
3. Third, the `AuthorizationFilter` is invoked to authorize the request.
2. Second, xref:servlet/authentication/architecture.adoc[the authentication filters] are invoked to authenticate the request.
3. Third, xref:servlet/authorization/authorize-http-requests.adoc[the `AuthorizationFilter`] is invoked to authorize the request.
[NOTE]
====
@ -254,22 +257,7 @@ The list of filters is printed at DEBUG level on the application startup, so you
[source,text,role="terminal"]
----
2023-06-14T08:55:22.321-03:00 DEBUG 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
2023-06-14T08:55:22.321-03:00 DEBUG 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [ DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter]
----
And that will give a pretty good idea of the security filters that are configured for <<servlet-securityfilterchain,each filter chain>>.
@ -279,13 +267,52 @@ That is helpful to see if the filter you have added is invoked for a particular
To do that, you can configure your application to <<servlet-logging,log the security events>>.
[[adding-custom-filter]]
=== Adding a Custom Filter to the Filter Chain
=== Adding Filters to the Filter Chain
Most of the time, the default security filters are enough to provide security to your application.
However, there might be times that you want to add a custom `Filter` to the security filter chain.
Most of the time, the default <<servlet-security-filters>> are enough to provide security to your application.
However, there might be times that you want to add a custom `Filter` to the <<servlet-securityfilterchain>>.
javadoc:org.springframework.security.config.annotation.web.builders.HttpSecurity[] comes with three methods for adding filters:
* `#addFilterBefore(Filter, Class<?>)` adds your filter before another filter
* `#addFilterAfter(Filter, Class<?>)` adds your filter after another filter
* `#addFilterAt(Filter, Class<?>)` replaces another filter with your 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.
Please take a look at the following key events that occur in the filter chain:
1. xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContext`] is loaded from the session
2. Request is protected from common exploits; xref:features/exploits/headers.adoc[secure headers], xref:servlet/integrations/cors.adoc[CORS], xref:servlet/exploits/csrf.adoc[CSRF]
3. Request is xref:servlet/authentication/architecture.adoc[authenticated]
4. Request is xref:servlet/authorization/architecture.adoc[authorized]
Consider which events you need to have happened in order to locate your filter.
The following is a rule of thumb:
[cols="1,1,1"]
|===
| If your filter is a(n) | Then place it after | As these events have already occurred
| exploit protection filter
| SecurityContextHolderFilter
| 1
| authentication filter
| LogoutFilter
| 1, 2
| authorization filter
| AnonymousAuthenticationFilter
| 1, 2, 3
|===
[TIP]
Most commonly, applications add a custom authentication.
This means they should be placed after xref:servlet/authentication/logout.adoc[`LogoutFilter`].
For example, let's say that you want to add a `Filter` that gets a tenant id header and check if the current user has access to that tenant.
The previous description already gives us a clue on where to add the filter, since we need to know the current user, we need to add it after the authentication filters.
First, let's create the `Filter`:
@ -335,7 +362,11 @@ The sample code above does the following:
Instead of implementing `Filter`, you can extend from {spring-framework-api-url}org/springframework/web/filter/OncePerRequestFilter.html[OncePerRequestFilter] which is a base class for filters that are only invoked once per request and provides a `doFilterInternal` method with the `HttpServletRequest` and `HttpServletResponse` parameters.
====
Now, we need to add the filter to the security filter chain.
Now, you need to add the filter to the <<servlet-securityfilterchain>>.
The previous description already gives us a clue on where to add the filter, since we need to know the current user, we need to add it after the authentication filters.
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::
@ -346,7 +377,7 @@ Java::
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class); <1>
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); <1>
return http.build();
}
----
@ -359,23 +390,26 @@ Kotlin::
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) <1>
.addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) <1>
return http.build()
}
----
======
<1> Use `HttpSecurity#addFilterBefore` to add the `TenantFilter` before the `AuthorizationFilter`.
<1> Use `HttpSecurity#addFilterAfter` to add the `TenantFilter` after the `AnonymousAuthenticationFilter`.
By adding the filter before the `AuthorizationFilter` we are making sure that the `TenantFilter` is invoked after the authentication filters.
You can also use `HttpSecurity#addFilterAfter` to add the filter after a particular filter or `HttpSecurity#addFilterAt` to add the filter at a particular filter position in the filter chain.
By adding the filter after the xref:servlet/authentication/anonymous.adoc[`AnonymousAuthenticationFilter`] we are making sure that the `TenantFilter` is invoked after the authentication filters.
And that's it, now the `TenantFilter` will be invoked in the filter chain and will check if the current user has access to the tenant id.
Be careful when you declare your filter as a Spring bean, either by annotating it with `@Component` or by declaring it as a bean in your configuration, because Spring Boot will automatically {spring-boot-reference-url}reference/web/servlet.html#web.servlet.embedded-container.servlets-filters-listeners.beans[register it with the embedded container].
==== Declaring Your Filter as a Bean
When you declare a `Filter` as a Spring bean, either by annotating it with `@Component` or by declaring it as a bean in your configuration, Spring Boot automatically {spring-boot-reference-url}reference/web/servlet.html#web.servlet.embedded-container.servlets-filters-listeners.beans[registers it with the embedded container].
That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order.
If you still want to declare your filter as a Spring bean to take advantage of dependency injection for example, and avoid the duplicate invocation, you can tell Spring Boot to not register it with the container by declaring a `FilterRegistrationBean` bean and setting its `enabled` property to `false`:
Because of that, filters are often not Spring beans.
However, if your filter needs to be a Spring bean (to take advantage of dependency injection, for example) you can tell Spring Boot to not register it with the container by declaring a `FilterRegistrationBean` bean and setting its `enabled` property to `false`:
[source,java]
----
@ -387,6 +421,141 @@ public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilte
}
----
This makes so that `HttpSecurity` is the only one adding it.
==== 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()
}
----
======
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()
}
----
======
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()
}
----
======
In this case, remove the call to `httpBasic` since you are constructing `BasicAuthenticationFilter` yourself.
[TIP]
====
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())
----
====
[[servlet-exceptiontranslationfilter]]
== Handling Security Exceptions