You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
348 lines
14 KiB
348 lines
14 KiB
|
|
[[kotlin-config]] |
|
= Kotlin Configuration |
|
|
|
Spring Security Kotlin configuration has been available since Spring Security 5.3. |
|
It lets users configure Spring Security by using a native Kotlin DSL. |
|
|
|
[NOTE] |
|
==== |
|
Spring Security provides https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/kotlin/hello-security[a sample application] to demonstrate the use of Spring Security Kotlin Configuration. |
|
==== |
|
|
|
[[kotlin-config-httpsecurity]] |
|
== HttpSecurity |
|
|
|
How does Spring Security know that we want to require all users to be authenticated? |
|
How does Spring Security know we want to support form-based authentication? |
|
There is a configuration class (called `SecurityFilterChain`) that is being invoked behind the scenes. |
|
It is configured with the following default implementation: |
|
|
|
[source,kotlin] |
|
---- |
|
import org.springframework.security.config.annotation.web.invoke |
|
|
|
@Bean |
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain { |
|
http { |
|
authorizeHttpRequests { |
|
authorize(anyRequest, authenticated) |
|
} |
|
formLogin { } |
|
httpBasic { } |
|
} |
|
return http.build() |
|
} |
|
---- |
|
|
|
[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 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 HTTP Basic authentication |
|
|
|
Note that this configuration parallels the XML namespace configuration: |
|
|
|
[source,xml] |
|
---- |
|
<http> |
|
<intercept-url pattern="/**" access="authenticated"/> |
|
<form-login /> |
|
<http-basic /> |
|
</http> |
|
---- |
|
|
|
=== Multiple HttpSecurity Instances |
|
|
|
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 `<http>` 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/`: |
|
|
|
[[multiple-httpsecurity-instances-kotlin]] |
|
[source,kotlin] |
|
---- |
|
import org.springframework.security.config.annotation.web.invoke |
|
|
|
@Configuration |
|
@EnableWebSecurity |
|
class MultiHttpSecurityConfig { |
|
@Bean <1> |
|
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 |
|
} |
|
|
|
@Bean |
|
@Order(1) <2> |
|
open fun apiFilterChain(http: HttpSecurity): SecurityFilterChain { |
|
http { |
|
securityMatcher("/api/**") <3> |
|
authorizeHttpRequests { |
|
authorize(anyRequest, hasRole("ADMIN")) |
|
} |
|
httpBasic { } |
|
} |
|
return http.build() |
|
} |
|
|
|
@Bean <4> |
|
open fun formLoginFilterChain(http: HttpSecurity): SecurityFilterChain { |
|
http { |
|
authorizeHttpRequests { |
|
authorize(anyRequest, authenticated) |
|
} |
|
formLogin { } |
|
} |
|
return http.build() |
|
} |
|
} |
|
---- |
|
<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 begin with `/api/`. |
|
<4> Create another instance of `SecurityFilterChain`. |
|
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 <<multiple-httpsecurity-instances-kotlin,earlier example>>. |
|
==== |
|
|
|
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 <<choosing-security-matcher-request-matchers-kotlin,above example>>, 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.
|
|
|