From 9b1e9c5db9991535cf1fcb60211eef78a906d342 Mon Sep 17 00:00:00 2001
From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com>
Date: Fri, 24 May 2024 16:30:30 -0500
Subject: [PATCH] Polish and sync java and kotlin configuration docs
Issue gh-15029
---
.../pages/servlet/configuration/java.adoc | 12 +-
.../pages/servlet/configuration/kotlin.adoc | 257 +++++++++++++++++-
2 files changed, 252 insertions(+), 17 deletions(-)
diff --git a/docs/modules/ROOT/pages/servlet/configuration/java.adoc b/docs/modules/ROOT/pages/servlet/configuration/java.adoc
index 1b3aa39e59..0b8e45dca8 100644
--- a/docs/modules/ROOT/pages/servlet/configuration/java.adoc
+++ b/docs/modules/ROOT/pages/servlet/configuration/java.adoc
@@ -185,10 +185,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
The default configuration (shown in the preceding example):
* Ensures that any request to our application requires the user to be authenticated
-* Lets users authenticate with form based login
+* Lets users authenticate with form-based login
* Lets users authenticate with HTTP Basic authentication
-Note that this configuration is parallels the XML Namespace configuration:
+Note that this configuration parallels the XML namespace configuration:
[source,xml]
----
@@ -206,7 +206,7 @@ This approach allows us to define distinct security configurations tailored to s
We can configure multiple `HttpSecurity` instances just as we can have multiple `` blocks in XML.
The key is to register multiple `SecurityFilterChain` ``@Bean``s.
-The following example has a different configuration for URLs that begin with `/api/`.
+The following example has a different configuration for URLs that begin with `/api/`:
[[multiple-httpsecurity-instances-java]]
[source,java]
@@ -216,7 +216,6 @@ The following example has a different configuration for URLs that begin with `/a
public class MultiHttpSecurityConfig {
@Bean <1>
public UserDetailsService userDetailsService() throws Exception {
- // ensure the passwords are encoded properly
UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("password").roles("USER").build());
@@ -411,7 +410,6 @@ public class BankingSecurityConfig {
@Bean <1>
public UserDetailsService userDetailsService() {
- // ensure the passwords are encoded properly
UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user1").password("password").roles("USER", "VIEW_BALANCE").build());
@@ -449,7 +447,7 @@ public class BankingSecurityConfig {
@Bean <4>
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
- String[] allowedPaths = { "/user-login", "/user-logout", "/notices", "/contact", "/register" };
+ String[] allowedPaths = { "/", "/user-login", "/user-logout", "/notices", "/contact", "/register" };
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(allowedPaths).permitAll()
@@ -478,7 +476,7 @@ public class BankingSecurityConfig {
This filter chain does not define any authentication because the next (default) filter chain contains that configuration.
<4> Lastly, create an additional `SecurityFilterChain` instance without an `@Order` annotation.
This configuration will handle requests not covered by the other filter chains and will be processed last (no `@Order` defaults to last).
- Requests that match `/user-login`, `/user-logout`, `/notices`, `/contact` and `/register` allow access without authentication.
+ Requests that match `/`, `/user-login`, `/user-logout`, `/notices`, `/contact` and `/register` allow access without authentication.
Any other requests require the user to be authenticated to access any URL not explicitly allowed or protected by other filter chains.
[[jc-custom-dsls]]
diff --git a/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc b/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc
index 8441d751e7..91d62ba0e1 100644
--- a/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc
+++ b/docs/modules/ROOT/pages/servlet/configuration/kotlin.adoc
@@ -38,7 +38,7 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
[NOTE]
Make sure to import the `org.springframework.security.config.annotation.web.invoke` function to enable the Kotlin DSL in your class, as the IDE will not always auto-import the method, causing compilation issues.
-The default configuration (shown in the preceding listing):
+The default configuration (shown in the preceding example):
* Ensures that any request to our application requires the user to be authenticated
* Lets users authenticate with form-based login
@@ -55,12 +55,16 @@ Note that this configuration parallels the XML namespace configuration:
----
-== Multiple HttpSecurity Instances
+=== Multiple HttpSecurity Instances
-We can configure multiple `HttpSecurity` instances, just as we can have multiple `` blocks.
+To effectively manage security in an application where certain areas need different protection, we can employ multiple filter chains alongside the `securityMatcher` DSL method.
+This approach allows us to define distinct security configurations tailored to specific parts of the application, enhancing overall application security and control.
+
+We can configure multiple `HttpSecurity` instances just as we can have multiple `` blocks in XML.
The key is to register multiple `SecurityFilterChain` ``@Bean``s.
-The following example has a different configuration for URLs that start with `/api/`:
+The following example has a different configuration for URLs that begin with `/api/`:
+[[multiple-httpsecurity-instances-kotlin]]
[source,kotlin]
----
import org.springframework.security.config.annotation.web.invoke
@@ -69,16 +73,16 @@ import org.springframework.security.config.annotation.web.invoke
@EnableWebSecurity
class MultiHttpSecurityConfig {
@Bean <1>
- public fun userDetailsService(): UserDetailsService {
- val users: User.UserBuilder = User.withDefaultPasswordEncoder()
+ open fun userDetailsService(): UserDetailsService {
+ val users = User.withDefaultPasswordEncoder()
val manager = InMemoryUserDetailsManager()
manager.createUser(users.username("user").password("password").roles("USER").build())
manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build())
return manager
}
- @Order(1) <2>
@Bean
+ @Order(1) <2>
open fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher("/api/**") <3>
@@ -102,10 +106,243 @@ class MultiHttpSecurityConfig {
}
}
----
-
<1> Configure Authentication as usual.
<2> Create an instance of `SecurityFilterChain` that contains `@Order` to specify which `SecurityFilterChain` should be considered first.
-<3> The `http.securityMatcher` states that this `HttpSecurity` is applicable only to URLs that start with `/api/`
+<3> The `http.securityMatcher()` states that this `HttpSecurity` is applicable only to URLs that begin with `/api/`.
<4> Create another instance of `SecurityFilterChain`.
-If the URL does not start with `/api/`, this configuration is used.
+If the URL does not begin with `/api/`, this configuration is used.
This configuration is considered after `apiFilterChain`, since it has an `@Order` value after `1` (no `@Order` defaults to last).
+
+=== Choosing `securityMatcher` or `requestMatchers`
+
+A common question is:
+
+> What is the difference between the `http.securityMatcher()` method and `requestMatchers()` used for request authorization (i.e. inside of `http.authorizeHttpRequests()`)?
+
+To answer this question, it helps to understand that each `HttpSecurity` instance used to build a `SecurityFilterChain` contains a `RequestMatcher` to match incoming requests.
+If a request does not match a `SecurityFilterChain` with higher priority (e.g. `@Order(1)`), the request can be tried against a filter chain with lower priority (e.g. no `@Order`).
+
+[NOTE]
+====
+The matching logic for multiple filter chains is performed by the xref:servlet/architecture.adoc#servlet-filterchainproxy[`FilterChainProxy`].
+====
+
+The default `RequestMatcher` matches *any request* to ensure Spring Security protects *all requests by default*.
+
+[NOTE]
+====
+Specifying a `securityMatcher` overrides this default.
+====
+
+[WARNING]
+====
+If no filter chain matches a particular request, the request is *not protected* by Spring Security.
+====
+
+The following example demonstrates a single filter chain that only protects requests that begin with `/secured/`:
+
+[[choosing-security-matcher-request-matchers-kotlin]]
+[source,kotlin]
+----
+import org.springframework.security.config.annotation.web.invoke
+
+@Configuration
+@EnableWebSecurity
+class PartialSecurityConfig {
+ @Bean
+ open fun userDetailsService(): UserDetailsService {
+ // ...
+ }
+
+ @Bean
+ open fun securedFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http {
+ securityMatcher("/secured/**") <1>
+ authorizeHttpRequests {
+ authorize("/secured/user", hasRole("USER")) <2>
+ authorize("/secured/admin", hasRole("ADMIN")) <3>
+ authorize(anyRequest, authenticated) <4>
+ }
+ httpBasic { }
+ formLogin { }
+ }
+ return http.build()
+ }
+}
+----
+<1> Requests that begin with `/secured/` will be protected but any other requests are not protected.
+<2> Requests to `/secured/user` require the `ROLE_USER` authority.
+<3> Requests to `/secured/admin` require the `ROLE_ADMIN` authority.
+<4> Any other requests (such as `/secured/other`) simply require an authenticated user.
+
+[TIP]
+====
+It is _recommended_ to provide a `SecurityFilterChain` that does not specify any `securityMatcher` to ensure the entire application is protected, as demonstrated in the <>.
+====
+
+Notice that the `requestMatchers` method only applies to individual authorization rules.
+Each request listed there must also match the overall `securityMatcher` for this particular `HttpSecurity` instance used to create the `SecurityFilterChain`.
+Using `anyRequest()` in this example matches all other requests within this particular `SecurityFilterChain` (which must begin with `/secured/`).
+
+[NOTE]
+====
+See xref:servlet/authorization/authorize-http-requests.adoc[Authorize HttpServletRequests] for more information on `requestMatchers`.
+====
+
+=== `SecurityFilterChain` Endpoints
+
+Several filters in the `SecurityFilterChain` directly provide endpoints, such as the `UsernamePasswordAuthenticationFilter` which is set up by `http.formLogin()` and provides the `POST /login` endpoint.
+In the <>, the `/login` endpoint is not matched by `http.securityMatcher("/secured/**")` and therefore that application would not have any `GET /login` or `POST /login` endpoint.
+Such requests would return `404 Not Found`.
+This is often surprising to users.
+
+Specifying `http.securityMatcher()` affects what requests are matched by that `SecurityFilterChain`.
+However, it does not automatically affect endpoints provided by the filter chain.
+In such cases, you may need to customize the URL of any endpoints you would like the filter chain to provide.
+
+The following example demonstrates a configuration that secures requests that begin with `/secured/` and denies all other requests, while also customizing endpoints provided by the `SecurityFilterChain`:
+
+[[security-filter-chain-endpoints-kotlin]]
+[source,kotlin]
+----
+import org.springframework.security.config.annotation.web.invoke
+
+@Configuration
+@EnableWebSecurity
+class SecuredSecurityConfig {
+ @Bean
+ open fun userDetailsService(): UserDetailsService {
+ // ...
+ }
+
+ @Bean
+ @Order(1)
+ open fun securedFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http {
+ securityMatcher("/secured/**") <1>
+ authorizeHttpRequests {
+ authorize(anyRequest, authenticated) <2>
+ }
+ formLogin { <3>
+ loginPage = "/secured/login"
+ loginProcessingUrl = "/secured/login"
+ permitAll = true
+ }
+ logout { <4>
+ logoutUrl = "/secured/logout"
+ logoutSuccessUrl = "/secured/login?logout"
+ permitAll = true
+ }
+ }
+ return http.build()
+ }
+
+ @Bean
+ open fun defaultFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http {
+ authorizeHttpRequests {
+ authorize(anyRequest, denyAll) <5>
+ }
+ }
+ return http.build()
+ }
+}
+----
+<1> Requests that begin with `/secured/` will be protected by this filter chain.
+<2> Requests that begin with `/secured/` require an authenticated user.
+<3> Customize form login to prefix URLs with `/secured/`.
+<4> Customize logout to prefix URLs with `/secured/`.
+<5> All other requests will be denied.
+
+[NOTE]
+====
+This example customizes the login and logout pages, which disables Spring Security's generated pages.
+You must xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form-custom[provide your own] custom endpoints for `GET /secured/login` and `GET /secured/logout`.
+Note that Spring Security still provides `POST /secured/login` and `POST /secured/logout` endpoints for you.
+====
+
+=== Real World Example
+
+The following example demonstrates a slightly more real-world configuration putting all of these elements together:
+
+[[real-world-example-kotlin]]
+[source,kotlin]
+----
+import org.springframework.security.config.annotation.web.invoke
+
+@Configuration
+@EnableWebSecurity
+class BankingSecurityConfig {
+ @Bean <1>
+ open fun userDetailsService(): UserDetailsService {
+ val users = User.withDefaultPasswordEncoder()
+ val manager = InMemoryUserDetailsManager()
+ manager.createUser(users.username("user1").password("password").roles("USER", "VIEW_BALANCE").build())
+ manager.createUser(users.username("user2").password("password").roles("USER").build())
+ manager.createUser(users.username("admin").password("password").roles("ADMIN").build())
+ return manager
+ }
+
+ @Bean
+ @Order(1) <2>
+ open fun approvalsSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ val approvalsPaths = arrayOf("/accounts/approvals/**", "/loans/approvals/**", "/credit-cards/approvals/**")
+ http {
+ securityMatcher(approvalsPaths)
+ authorizeHttpRequests {
+ authorize(anyRequest, hasRole("ADMIN"))
+ }
+ httpBasic { }
+ }
+ return http.build()
+ }
+
+ @Bean
+ @Order(2) <3>
+ open fun bankingSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ val bankingPaths = arrayOf("/accounts/**", "/loans/**", "/credit-cards/**", "/balances/**")
+ val viewBalancePaths = arrayOf("/balances/**")
+ http {
+ securityMatcher(bankingPaths)
+ authorizeHttpRequests {
+ authorize(viewBalancePaths, hasRole("VIEW_BALANCE"))
+ authorize(anyRequest, hasRole("USER"))
+ }
+ }
+ return http.build()
+ }
+
+ @Bean <4>
+ open fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ val allowedPaths = arrayOf("/", "/user-login", "/user-logout", "/notices", "/contact", "/register")
+ http {
+ authorizeHttpRequests {
+ authorize(allowedPaths, permitAll)
+ authorize(anyRequest, authenticated)
+ }
+ formLogin {
+ loginPage = "/user-login"
+ loginProcessingUrl = "/user-login"
+ }
+ logout {
+ logoutUrl = "/user-logout"
+ logoutSuccessUrl = "/?logout"
+ }
+ }
+ return http.build()
+ }
+}
+----
+<1> Begin by configuring authentication settings.
+<2> Define a `SecurityFilterChain` instance with `@Order(1)`, which means that this filter chain will have the highest priority.
+ This filter chain applies only to requests that begin with `/accounts/approvals/`, `/loans/approvals/` or `/credit-cards/approvals/`.
+ Requests to this filter chain require the `ROLE_ADMIN` authority and allow HTTP Basic Authentication.
+<3> Next, create another `SecurityFilterChain` instance with `@Order(2)` which will be considered second.
+ This filter chain applies only to requests that begin with `/accounts/`, `/loans/`, `/credit-cards/`, or `/balances/`.
+ Notice that because this filter chain is second, any requests that include `/approvals/` will match the previous filter chain and will *not* be matched by this filter chain.
+ Requests to this filter chain require the `ROLE_USER` authority.
+ This filter chain does not define any authentication because the next (default) filter chain contains that configuration.
+<4> Lastly, create an additional `SecurityFilterChain` instance without an `@Order` annotation.
+ This configuration will handle requests not covered by the other filter chains and will be processed last (no `@Order` defaults to last).
+ Requests that match `/`, `/user-login`, `/user-logout`, `/notices`, `/contact` and `/register` allow access without authentication.
+ Any other requests require the user to be authenticated to access any URL not explicitly allowed or protected by other filter chains.