123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- [[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.
- [[modular-httpsecuritydsl-configuration]]
- == Modular HttpSecurityDsl Configuration
- Many users prefer that their Spring Security configuration lives in a centralized place and will choose to configure it in a single `SecurityFilterChain` instance.
- However, there are times that users may want to modularize the configuration.
- This can be done using:
- * xref:#httpsecuritydsl-bean[HttpSecurityDsl.() -> Unit Beans]
- * xref:#top-level-dsl-bean[Top Level Security Dsl Beans]
- NOTE: Since the Spring Security Kotlin Dsl (`HttpSecurityDsl`) uses `HttpSecurity`, all of the Java xref:./kotlin.adoc#modular-bean-configuration[Modular Bean Customization] is applied before xref:#modular-httpsecuritydsl-configuration[Modular HttpSecurity Configuration].
- [[httpsecuritydsl-bean]]
- === HttpSecurityDsl.() -> Unit Beans
- If you would like to modularize your security configuration you can place logic in a `HttpSecurityDsl.() -> Unit` Bean.
- For example, the following configuration will ensure all `HttpSecurityDsl` instances are configured to:
- include-code::./HttpSecurityDslBeanConfiguration[tag=httpSecurityDslBean,indent=0]
- <1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'`
- <2> xref:servlet/exploits/http.adoc#servlet-http-redirect[Redirect any request to https]
- [[top-level-dsl-bean]]
- === Top Level Security Dsl Beans
- If you prefer to have further modularization of your security configuration, Spring Security will automatically apply any top level Security Dsl Beans.
- A top level Security Dsl can be summarized as any class Dsl class that matches `public HttpSecurityDsl.*(<Dsl>)`.
- This translates to any Security Dsl that is a single argument to a public method on `HttpSecurityDsl`.
- A few examples can help to clarify.
- If `ContentTypeOptionsDsl.() -> Unit` is published as a Bean, it will not be be automatically applied because it is an argument to `HeadersDsl#contentTypeOptions(ContentTypeOptionsDsl.() -> Unit)` and is not an argument to a method defined on `HttpSecurityDsl`.
- However, if `HeadersDsl.() -> Unit` is published as a Bean, it will be automatically applied because it is an argument to `HttpSecurityDsl.headers(HeadersDsl.() -> Unit)`.
- For example, the following configuration ensure all `HttpSecurityDsl` instances are configured to:
- include-code::./TopLevelDslBeanConfiguration[tag=headersSecurity,indent=0]
- <1> Set the xref:servlet/exploits/headers.adoc#servlet-headers-csp[Content Security Policy] to `object-src 'none'`
- [[dsl-bean-ordering]]
- === Dsl Bean Ordering
- First, all xref:servlet/configuration/java.adoc#modular-httpsecurity-configuration[Modular HttpSecurity Configuration] is applied since the Kotlin Dsl uses an `HttpSecurity` Bean.
- Second, each xref:#httpsecuritydsl-bean[HttpSecurityDsl.() -> Unit Beans] is applied using https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#orderedStream()[ObjectProvider#orderedStream()].
- This means that if there are multiple `HttpSecurity.() -> Unit` Beans, the https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[@Order] annotation can be added to the Bean definitions to control the ordering.
- Next, every xref:#top-level-dsl-bean[Top Level Security Dsl Beans] type is looked up and each is is applied using `ObjectProvider#orderedStream()`.
- If there is are differt types of top level security Beans (.e.g. `HeadersDsl.() -> Unit` and `HttpsRedirectDsl.() -> Unit`), then the order that each Dsl type is invoked is undefined.
- However, the order that each instance of of the same top level security Bean type is defined by `ObjectProvider#orderedStream()` and can be controlled using `@Order` on the Bean the definitions.
- Finally, the `HttpSecurityDsl` Bean is injected as a Bean.
- All `*Dsl.() -> Unit` Beans are applied before the `HttpSecurityDsl` Bean is created.
- This allows overriding the customizations provided by the `*Dsl.() -> Unit` Beans.
- You can find an example below that illustrates the ordering:
- include-code::./DslBeanOrderingConfiguration[tag=sample,indent=0]
- <1> All xref:servlet/configuration/java.adoc#modular-httpsecurity-configuration[Modular HttpSecurity Configuration] is applied since the Kotlin Dsl uses an `HttpSecurity` Bean.
- <2> All `HttpSecurity.() -> Unit` instances are applied.
- The `adminAuthorization` Bean has the highest `@Order` so it is applied first.
- If there are no `@Order` annotations on the `HttpSecurity.() -> Unit` Beans or the `@Order` annotations had the same value, then the order that the `HttpSecurity.() -> Unit` instances are applied is undefined.
- <3> The `userAuthorization` is applied next due to being an instance of `HttpSecurity.() -> Unit`
- <4> The order that the `*Dsl.() -> Unit` types are undefined.
- In this example, the order of `contentSecurityPolicy`, `contentTypeOptions`, and `httpsRedirect` are undefined.
- If `@Order(Ordered.HIGHEST_PRECEDENCE)` was added to `contentTypeOptions`, then we would know that `contentTypeOptions` is before `contentSecurityPolicy` (they are the same type), but we do not know if `httpsRedirect` is before or after the `HeadersDsl.() -> Unit` Beans.
- <5> After all of the `*Dsl.() -> Unit` Beans are applied, the `HttpSecurityDsl` is passed in as a Bean.
|