Skip to content

Commit c30baca

Browse files
Improve Security Filters Documentation
Closes gh-8167
1 parent a104dec commit c30baca

File tree

3 files changed

+270
-41
lines changed

3 files changed

+270
-41
lines changed

docs/modules/ROOT/pages/servlet/architecture.adoc

Lines changed: 265 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -164,46 +164,224 @@ In fact, a `SecurityFilterChain` might have zero security ``Filter``s if the app
164164
[[servlet-security-filters]]
165165
== Security Filters
166166

167+
Spring Security uses a number of Servlet Filters (https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.pdf[Jakarta Servlet Spec, Chapter 6]) to provide security to your application.
167168
The Security Filters are inserted into the <<servlet-filterchainproxy>> with the <<servlet-securityfilterchain>> API.
168-
The <<servlet-filters-review,order of ``Filter``>>s matters.
169+
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.
170+
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.
169171
It is typically not necessary to know the ordering of Spring Security's ``Filter``s.
170-
However, there are times that it is beneficial to know the ordering
171-
172-
Below is a comprehensive list of Spring Security Filter ordering:
173-
174-
* xref:servlet/authentication/session-management.adoc#session-mgmt-force-session-creation[`ForceEagerSessionCreationFilter`]
175-
* ChannelProcessingFilter
176-
* WebAsyncManagerIntegrationFilter
177-
* SecurityContextPersistenceFilter
178-
* HeaderWriterFilter
179-
* CorsFilter
180-
* CsrfFilter
181-
* LogoutFilter
182-
* OAuth2AuthorizationRequestRedirectFilter
183-
* Saml2WebSsoAuthenticationRequestFilter
184-
* X509AuthenticationFilter
185-
* AbstractPreAuthenticatedProcessingFilter
186-
* CasAuthenticationFilter
187-
* OAuth2LoginAuthenticationFilter
188-
* Saml2WebSsoAuthenticationFilter
189-
* xref:servlet/authentication/passwords/form.adoc#servlet-authentication-usernamepasswordauthenticationfilter[`UsernamePasswordAuthenticationFilter`]
190-
* OpenIDAuthenticationFilter
191-
* DefaultLoginPageGeneratingFilter
192-
* DefaultLogoutPageGeneratingFilter
193-
* ConcurrentSessionFilter
194-
* xref:servlet/authentication/passwords/digest.adoc#servlet-authentication-digest[`DigestAuthenticationFilter`]
195-
* BearerTokenAuthenticationFilter
196-
* xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[`BasicAuthenticationFilter`]
197-
* <<requestcacheawarefilter,RequestCacheAwareFilter>>
198-
* SecurityContextHolderAwareRequestFilter
199-
* JaasApiIntegrationFilter
200-
* RememberMeAuthenticationFilter
201-
* AnonymousAuthenticationFilter
202-
* OAuth2AuthorizationCodeGrantFilter
203-
* SessionManagementFilter
204-
* <<servlet-exceptiontranslationfilter,`ExceptionTranslationFilter`>>
205-
* xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`]
206-
* SwitchUserFilter
172+
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].
173+
174+
To exemplify the above paragraph, let's consider the following security configuration:
175+
176+
====
177+
.Java
178+
[source,java,role="primary"]
179+
----
180+
@Configuration
181+
@EnableWebSecurity
182+
public class SecurityConfig {
183+
184+
@Bean
185+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
186+
http
187+
.csrf(Customizer.withDefaults())
188+
.authorizeHttpRequests(authorize -> authorize
189+
.anyRequest().authenticated()
190+
)
191+
.httpBasic(Customizer.withDefaults())
192+
.formLogin(Customizer.withDefaults());
193+
return http.build();
194+
}
195+
196+
}
197+
----
198+
.Kotlin
199+
[source,kotlin,role="secondary"]
200+
----
201+
import org.springframework.security.config.web.servlet.invoke
202+
203+
@Configuration
204+
@EnableWebSecurity
205+
class SecurityConfig {
206+
207+
@Bean
208+
fun filterChain(http: HttpSecurity): SecurityFilterChain {
209+
http {
210+
csrf { }
211+
authorizeHttpRequests {
212+
authorize(anyRequest, authenticated)
213+
}
214+
httpBasic { }
215+
formLogin { }
216+
}
217+
return http.build()
218+
}
219+
220+
}
221+
----
222+
====
223+
224+
The above configuration will result in the following `Filter` ordering:
225+
226+
[cols="1,1", options="header"]
227+
|====
228+
| Filter | Added by
229+
| xref:servlet/exploits/csrf.adoc[CsrfFilter] | `HttpSecurity#csrf`
230+
| xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form[UsernamePasswordAuthenticationFilter] | `HttpSecurity#formLogin`
231+
| xref:servlet/authentication/passwords/basic.adoc[BasicAuthenticationFilter] | `HttpSecurity#httpBasic`
232+
| xref:servlet/authorization/authorize-http-requests.adoc[AuthorizationFilter] | `HttpSecurity#authorizeHttpRequests`
233+
|====
234+
235+
1. First, the `CsrfFilter` is invoked to protect against xref:servlet/exploits/csrf.adoc[CSRF attacks].
236+
2. Second, the authentication filters are invoked to authenticate the request.
237+
3. Third, the `AuthorizationFilter` is invoked to authorize the request.
238+
239+
[NOTE]
240+
====
241+
There might be other `Filter` instances that are not listed above.
242+
If you want to see the list of filters invoked for a particular request, you can <<servlet-print-filters,print them>>.
243+
====
244+
245+
[[servlet-print-filters]]
246+
=== Printing the Security Filters
247+
248+
Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request.
249+
For example, you want to make sure that the <<adding-custom-filter,filter you have added>> is in the list of the security filters.
250+
251+
The list of filters is printed at INFO level on the application startup, so you can see something like the following on the console output for example:
252+
253+
[source,text,role="terminal"]
254+
----
255+
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
256+
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
257+
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
258+
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
259+
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
260+
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
261+
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
262+
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
263+
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
264+
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
265+
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
266+
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
267+
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
268+
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
269+
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
270+
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
271+
----
272+
273+
And that will give a pretty good idea of the security filters that are configured for <<servlet-securityfilterchain,each filter chain>>.
274+
275+
But that is not all, you can also configure your application to print the invocation of each individual filter for each request.
276+
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.
277+
To do that, you can configure your application to <<servlet-logging,log the security events>>.
278+
279+
[[adding-custom-filter]]
280+
=== Adding a Custom Filter to the Filter Chain
281+
282+
Mostly of the times, the default security filters are enough to provide security to your application.
283+
However, there might be times that you want to add a custom `Filter` to the security filter chain.
284+
285+
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.
286+
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.
287+
288+
First, let's create the `Filter`:
289+
290+
[source,java]
291+
----
292+
import java.io.IOException;
293+
294+
import jakarta.servlet.Filter;
295+
import jakarta.servlet.FilterChain;
296+
import jakarta.servlet.ServletException;
297+
import jakarta.servlet.ServletRequest;
298+
import jakarta.servlet.ServletResponse;
299+
import jakarta.servlet.http.HttpServletRequest;
300+
import jakarta.servlet.http.HttpServletResponse;
301+
302+
import org.springframework.security.access.AccessDeniedException;
303+
304+
public class TenantFilter implements Filter {
305+
306+
@Override
307+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
308+
HttpServletRequest request = (HttpServletRequest) servletRequest;
309+
HttpServletResponse response = (HttpServletResponse) servletResponse;
310+
311+
String tenantId = request.getHeader("X-Tenant-Id"); <1>
312+
boolean hasAccess = isUserAllowed(tenantId); <2>
313+
if (hasAccess) {
314+
filterChain.doFilter(request, response); <3>
315+
return;
316+
}
317+
throw new AccessDeniedException("Access denied"); <4>
318+
}
319+
320+
}
321+
322+
----
323+
324+
The sample code above does the following:
325+
326+
<1> Get the tenant id from the request header.
327+
<2> Check if the current user has access to the tenant id.
328+
<3> If the user has access, then invoke the rest of the filters in the chain.
329+
<4> If the user does not have access, then throw an `AccessDeniedException`.
330+
331+
[TIP]
332+
====
333+
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.
334+
====
335+
336+
Now, we need to add the filter to the security filter chain.
337+
338+
====
339+
.Java
340+
[source,java,role="primary"]
341+
----
342+
@Bean
343+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
344+
http
345+
// ...
346+
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class); <1>
347+
return http.build();
348+
}
349+
----
350+
.Kotlin
351+
[source,kotlin,role="secondary"]
352+
----
353+
@Bean
354+
fun filterChain(http: HttpSecurity): SecurityFilterChain {
355+
http
356+
// ...
357+
.addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) <1>
358+
return http.build()
359+
}
360+
----
361+
====
362+
363+
<1> Use `HttpSecurity#addFilterBefore` to add the `TenantFilter` before the `AuthorizationFilter`.
364+
365+
By adding the filter before the `AuthorizationFilter` we are making sure that the `TenantFilter` is invoked after the authentication filters.
366+
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.
367+
368+
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.
369+
370+
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}web.html#web.servlet.embedded-container.servlets-filters-listeners.beans[register it with the embedded container].
371+
That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order.
372+
373+
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`:
374+
375+
[source,java]
376+
----
377+
@Bean
378+
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
379+
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
380+
registration.setEnabled(false);
381+
return registration;
382+
}
383+
----
384+
207385

208386
[[servlet-exceptiontranslationfilter]]
209387
== Handling Security Exceptions
@@ -333,3 +511,52 @@ XML::
333511
=== RequestCacheAwareFilter
334512

335513
The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <<requestcache,`RequestCache`>> to save the `HttpServletRequest`.
514+
515+
[[servlet-logging]]
516+
== Logging
517+
518+
Spring Security provides comprehensive logging of all security related events at the DEBUG and TRACE level.
519+
This can be very useful when debugging your application because for security measures Spring Security does not add any detail of why a request has been rejected to the response body.
520+
If you come across a 401 or 403 error, it is very likely that you will find a log message that will help you understand what is going on.
521+
522+
Let's consider an example where a user tries to make a `POST` request to a resource that has xref:servlet/exploits/csrf.adoc[CSRF protection] enabled without the CSRF token.
523+
With no logs, the user will see a 403 error with no explanation of why the request was rejected.
524+
However, if you enable logging for Spring Security, you will see a log message like this:
525+
526+
[source,text]
527+
----
528+
2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello
529+
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15)
530+
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15)
531+
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15)
532+
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15)
533+
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15)
534+
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
535+
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
536+
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
537+
----
538+
539+
It becomes clear that the CSRF token is missing and that is why the request is being denied.
540+
541+
To configure your application to log all the security events, you can add the following to your application:
542+
543+
====
544+
.application.properties in Spring Boot
545+
[source,properties,role="primary"]
546+
----
547+
logging.level.org.springframework.security=TRACE
548+
----
549+
.logback.xml
550+
[source,xml,role="secondary"]
551+
----
552+
<configuration>
553+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
554+
<!-- ... -->
555+
</appender>
556+
<!-- ... -->
557+
<logger name="org.springframework.security" level="trace" additivity="false">
558+
<appender-ref ref="Console" />
559+
</logger>
560+
</configuration>
561+
----
562+
====

docs/spring-security-docs.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ def generateAttributes() {
4040
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
4141
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
4242
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
43-
43+
def springBootReferenceUrl = "https://docs.spring.io/spring-boot/docs/$springBootVersion/reference/html/"
44+
4445
return ['gh-old-samples-url': ghOldSamplesUrl.toString(),
4546
'gh-samples-url': ghSamplesUrl.toString(),
4647
'gh-url': ghUrl.toString(),
4748
'security-api-url': securityApiUrl.toString(),
4849
'security-reference-url': securityReferenceUrl.toString(),
4950
'spring-framework-api-url': springFrameworkApiUrl.toString(),
5051
'spring-framework-reference-url': springFrameworkReferenceUrl.toString(),
51-
'spring-security-version': project.version]
52+
'spring-boot-reference-url': springBootReferenceUrl.toString(),
53+
'spring-security-version': project.version]
5254
+ resolvedVersions(project.configurations.testRuntimeClasspath)
5355
}
5456

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
aspectjVersion=1.9.19
22
springJavaformatVersion=0.0.39
3-
springBootVersion=2.4.2
3+
springBootVersion=2.7.12
44
springFrameworkVersion=5.3.28
55
openSamlVersion=3.4.6
66
version=5.8.5-SNAPSHOT

0 commit comments

Comments
 (0)