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.