diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 49d176c89b6..9e603d5bab3 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -85,6 +85,8 @@ dependencies { testImplementation ('org.springframework.data:spring-data-jpa') { exclude group: 'org.aspectj', module: 'aspectjrt' } + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor' testRuntimeOnly 'org.hsqldb:hsqldb' } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java index 83ff811ce7d..28bea1da218 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,8 @@ public void notPublisherPreAuthorizeFindByIdThenThrowsIllegalStateException() { .withMessage("The returnType class java.lang.String on public abstract java.lang.String " + "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService" + ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams" - + ".Publisher (i.e. Mono / Flux) in order to support Reactor Context"); + + ".Publisher (i.e. Mono / Flux) or the function must be a Kotlin coroutine " + + "function in order to support Reactor Context"); } @Test diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityTests.kt new file mode 100644 index 00000000000..3453b75f880 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityTests.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration + +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +@ContextConfiguration +class KotlinEnableReactiveMethodSecurityTests { + + @Autowired + var messageService: KotlinReactiveMessageService? = null + + @Test + fun suspendingGetResultWhenPermitAllThenSuccess() { + runBlocking { + assertThat(messageService!!.suspendingNoAuth()).isEqualTo("success") + } + } + + @Test + @WithMockUser(authorities = ["ROLE_ADMIN"]) + fun suspendingPreAuthorizeHasRoleWhenGrantedThenSuccess() { + runBlocking { + assertThat(messageService!!.suspendingPreAuthorizeHasRole()).isEqualTo("admin") + } + } + + @Test + fun suspendingPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() { + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { + runBlocking { + messageService!!.suspendingPreAuthorizeHasRole() + } + } + } + + @Test + @WithMockUser + fun suspendingPreAuthorizeBeanWhenGrantedThenSuccess() { + runBlocking { + assertThat(messageService!!.suspendingPreAuthorizeBean(true)).isEqualTo("check") + } + } + + @Test + fun suspendingPreAuthorizeBeanWhenNotAuthorizedThenDenied() { + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { + runBlocking { + messageService!!.suspendingPreAuthorizeBean(false) + } + } + } + + @Test + @WithMockUser("user") + fun suspendingPostAuthorizeWhenAuthorizedThenSuccess() { + runBlocking { + assertThat(messageService!!.suspendingPostAuthorizeContainsName()).isEqualTo("user") + } + } + + @Test + @WithMockUser("other-user") + fun suspendingPostAuthorizeWhenNotAuthorizedThenDenied() { + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { + runBlocking { + messageService!!.suspendingPostAuthorizeContainsName() + } + } + } + + @Test + @WithMockUser(authorities = ["ROLE_ADMIN"]) + fun suspendingFlowPreAuthorizeHasRoleWhenGrantedThenSuccess() { + runBlocking { + assertThat(messageService!!.suspendingFlowPreAuthorize().toList()).containsExactly(1, 2, 3) + } + } + + @Test + fun suspendingFlowPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() { + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { + runBlocking { + messageService!!.suspendingFlowPreAuthorize().collect() + } + } + } + + @Test + fun suspendingFlowPostAuthorizeWhenAuthorizedThenSuccess() { + runBlocking { + assertThat(messageService!!.suspendingFlowPostAuthorize(true).toList()).containsExactly(1, 2, 3) + } + } + + @Test + fun suspendingFlowPostAuthorizeWhenNotAuthorizedThenDenied() { + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { + runBlocking { + messageService!!.suspendingFlowPostAuthorize(false).collect() + } + } + } + + @Test + @WithMockUser(authorities = ["ROLE_ADMIN"]) + fun flowPreAuthorizeHasRoleWhenGrantedThenSuccess() { + runBlocking { + assertThat(messageService!!.flowPreAuthorize().toList()).containsExactly(1, 2, 3) + } + } + + @Test + fun flowPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() { + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { + runBlocking { + messageService!!.flowPreAuthorize().collect() + } + } + } + + @Test + fun flowPostAuthorizeWhenAuthorizedThenSuccess() { + runBlocking { + assertThat(messageService!!.flowPostAuthorize(true).toList()).containsExactly(1, 2, 3) + } + } + + @Test + fun flowPostAuthorizeWhenNotAuthorizedThenDenied() { + assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { + runBlocking { + messageService!!.flowPostAuthorize(false).collect() + } + } + } + + @EnableReactiveMethodSecurity + @Configuration + open class Config { + + @Bean + open fun messageService(): KotlinReactiveMessageServiceImpl { + return KotlinReactiveMessageServiceImpl() + } + + @Bean + open fun authz(): Authz { + return Authz() + } + + open class Authz { + fun check(r: Boolean): Boolean { + return r + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageService.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageService.kt new file mode 100644 index 00000000000..37ed70eb56a --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageService.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration + +import kotlinx.coroutines.flow.Flow + +interface KotlinReactiveMessageService { + + suspend fun suspendingNoAuth(): String + + suspend fun suspendingPreAuthorizeHasRole(): String + + suspend fun suspendingPreAuthorizeBean(id: Boolean): String + + suspend fun suspendingPostAuthorizeContainsName(): String + + suspend fun suspendingFlowPreAuthorize(): Flow + + suspend fun suspendingFlowPostAuthorize(id: Boolean): Flow + + fun flowPreAuthorize(): Flow + + fun flowPostAuthorize(id: Boolean): Flow +} diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageServiceImpl.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageServiceImpl.kt new file mode 100644 index 00000000000..f1261c2c064 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageServiceImpl.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.springframework.security.access.prepost.PostAuthorize +import org.springframework.security.access.prepost.PreAuthorize + +class KotlinReactiveMessageServiceImpl : KotlinReactiveMessageService { + + override suspend fun suspendingNoAuth(): String { + delay(1) + return "success" + } + + @PreAuthorize("hasRole('ADMIN')") + override suspend fun suspendingPreAuthorizeHasRole(): String { + delay(1) + return "admin" + } + + @PreAuthorize("@authz.check(#id)") + override suspend fun suspendingPreAuthorizeBean(id: Boolean): String { + delay(1) + return "check" + } + + @PostAuthorize("returnObject?.contains(authentication?.name)") + override suspend fun suspendingPostAuthorizeContainsName(): String { + delay(1) + return "user" + } + + @PreAuthorize("hasRole('ADMIN')") + override suspend fun suspendingFlowPreAuthorize(): Flow { + delay(1) + return flow { + for (i in 1..3) { + delay(1) + emit(i) + } + } + } + + @PostAuthorize("@authz.check(#id)") + override suspend fun suspendingFlowPostAuthorize(id: Boolean): Flow { + delay(1) + return flow { + for (i in 1..3) { + delay(1) + emit(i) + } + } + } + + @PreAuthorize("hasRole('ADMIN')") + override fun flowPreAuthorize(): Flow { + return flow { + for (i in 1..3) { + delay(1) + emit(i) + } + } + } + + @PostAuthorize("@authz.check(#id)") + override fun flowPostAuthorize(id: Boolean): Flow { + return flow { + for (i in 1..3) { + delay(1) + emit(i) + } + } + } +} diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index e85c287d66c..6661fb3fefa 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -26,6 +26,7 @@ dependencies { optional 'org.aspectj:aspectjrt' optional 'org.springframework:spring-jdbc' optional 'org.springframework:spring-tx' + optional 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor' testImplementation powerMock2Dependencies testImplementation 'commons-collections:commons-collections' diff --git a/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java index 4ed5e039fc1..d940ec25890 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ import java.lang.reflect.Method; import java.util.Collection; +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.reactive.AwaitKt; +import kotlinx.coroutines.reactive.ReactiveFlowKt; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.reactivestreams.Publisher; @@ -26,6 +29,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.CoroutinesUtils; +import org.springframework.core.KotlinDetector; +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.method.MethodSecurityMetadataSource; @@ -38,9 +46,11 @@ /** * A {@link MethodInterceptor} that supports {@link PreAuthorize} and - * {@link PostAuthorize} for methods that return {@link Mono} or {@link Flux} + * {@link PostAuthorize} for methods that return {@link Mono} or {@link Flux} and Kotlin + * coroutine functions. * * @author Rob Winch + * @author Eleftheria Stein * @since 5.0 */ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor { @@ -54,6 +64,10 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor private final PostInvocationAuthorizationAdvice postAdvice; + private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; + + private static final int RETURN_TYPE_METHOD_PARAMETER_INDEX = -1; + /** * Creates a new instance * @param attributeSource the {@link MethodSecurityMetadataSource} to use @@ -75,10 +89,18 @@ public PrePostAdviceReactiveMethodInterceptor(MethodSecurityMetadataSource attri public Object invoke(final MethodInvocation invocation) { Method method = invocation.getMethod(); Class returnType = method.getReturnType(); - Assert.state(Publisher.class.isAssignableFrom(returnType), + + boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method); + boolean hasFlowReturnType = COROUTINES_FLOW_CLASS_NAME + .equals(new MethodParameter(method, RETURN_TYPE_METHOD_PARAMETER_INDEX).getParameterType().getName()); + boolean hasReactiveReturnType = Publisher.class.isAssignableFrom(returnType) || isSuspendingFunction + || hasFlowReturnType; + + Assert.state(hasReactiveReturnType, () -> "The returnType " + returnType + " on " + method + " must return an instance of org.reactivestreams.Publisher " - + "(i.e. Mono / Flux) in order to support Reactor Context"); + + "(i.e. Mono / Flux) or the function must be a Kotlin coroutine " + + "function in order to support Reactor Context"); Class targetClass = invocation.getThis().getClass(); Collection attributes = this.attributeSource.getAttributes(method, targetClass); PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes); @@ -98,6 +120,30 @@ public Object invoke(final MethodInvocation invocation) { return toInvoke.flatMapMany((auth) -> PrePostAdviceReactiveMethodInterceptor.>proceed(invocation) .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); } + if (hasFlowReturnType) { + Publisher publisher; + if (isSuspendingFunction) { + publisher = CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(), + invocation.getArguments()); + } + else { + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnType); + Assert.state(adapter != null, () -> "The returnType " + returnType + " on " + method + + " must have a org.springframework.core.ReactiveAdapter registered"); + publisher = adapter.toPublisher(PrePostAdviceReactiveMethodInterceptor.flowProceed(invocation)); + } + Flux response = toInvoke.flatMapMany((auth) -> Flux.from(publisher) + .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); + return KotlinDelegate.asFlow(response); + } + if (isSuspendingFunction) { + Mono response = toInvoke.flatMap((auth) -> Mono + .from(CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(), + invocation.getArguments())) + .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); + return KotlinDelegate.awaitSingleOrNull(response, + invocation.getArguments()[invocation.getArguments().length - 1]); + } return toInvoke.flatMapMany( (auth) -> Flux.from(PrePostAdviceReactiveMethodInterceptor.>proceed(invocation)) .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); @@ -112,6 +158,15 @@ private static > T proceed(final MethodInvocation invocat } } + private static Object flowProceed(final MethodInvocation invocation) { + try { + return invocation.proceed(); + } + catch (Throwable throwable) { + throw Exceptions.propagate(throwable); + } + } + private static PostInvocationAttribute findPostInvocationAttribute(Collection config) { for (ConfigAttribute attribute : config) { if (attribute instanceof PostInvocationAttribute) { @@ -130,4 +185,19 @@ private static PreInvocationAttribute findPreInvocationAttribute(Collection publisher) { + return ReactiveFlowKt.asFlow(publisher); + } + + private static Object awaitSingleOrNull(Publisher publisher, Object continuation) { + return AwaitKt.awaitSingleOrNull(publisher, (Continuation) continuation); + } + + } + } diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 08b230dd7cf..18bcca13a90 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -12,6 +12,7 @@ dependencies { api platform("io.rsocket:rsocket-bom:1.1.0") api platform("org.springframework.data:spring-data-bom:2020.0.7") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") + api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.3") api platform("com.fasterxml.jackson:jackson-bom:2.12.2") constraints { api "ch.qos.logback:logback-classic:1.2.3" diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc index cc60afaffbe..08e8ba2fa78 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc @@ -6,11 +6,13 @@ For example, this demonstrates how to retrieve the currently logged in user's me [NOTE] ==== -For this to work the return type of the method must be a `org.reactivestreams.Publisher` (i.e. `Mono`/`Flux`). +For this to work the return type of the method must be a `org.reactivestreams.Publisher` (i.e. `Mono`/`Flux`) or the function must be a Kotlin coroutine function. This is necessary to integrate with Reactor's `Context`. ==== -[source,java] +==== +.Java +[source,java,role="primary"] ---- Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); @@ -26,18 +28,48 @@ StepVerifier.create(messageByUsername) .verifyComplete(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER") + +val messageByUsername: Mono = ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Authentication::getName) + .flatMap(this::findMessageByUsername) // In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter` + .subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication)) + +StepVerifier.create(messageByUsername) + .expectNext("Hi user") + .verifyComplete() +---- +==== + with `this::findMessageByUsername` defined as: -[source,java] +==== +.Java +[source,java,role="primary"] ---- Mono findMessageByUsername(String username) { return Mono.just("Hi " + username); } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +fun findMessageByUsername(username: String): Mono { + return Mono.just("Hi $username") +} +---- +==== + Below is a minimal method security configuration when using method security in reactive applications. -[source,java] +==== +.Java +[source,java,role="primary"] ---- @EnableReactiveMethodSecurity public class SecurityConfig { @@ -57,9 +89,33 @@ public class SecurityConfig { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableReactiveMethodSecurity +class SecurityConfig { + @Bean + fun userDetailsService(): MapReactiveUserDetailsService { + val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder() + val rob = userBuilder.username("rob") + .password("rob") + .roles("USER") + .build() + val admin = userBuilder.username("admin") + .password("admin") + .roles("USER", "ADMIN") + .build() + return MapReactiveUserDetailsService(rob, admin) + } +} +---- +==== + Consider the following class: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Component public class HelloWorldMessageService { @@ -70,6 +126,37 @@ public class HelloWorldMessageService { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Component +class HelloWorldMessageService { + @PreAuthorize("hasRole('ADMIN')") + fun findMessage(): Mono { + return Mono.just("Hello World!") + } +} +---- +==== + +Or, the following class using Kotlin coroutines: + +==== +.Kotlin +[source,kotlin,role="primary"] +---- +@Component +class HelloWorldMessageService { + @PreAuthorize("hasRole('ADMIN')") + suspend fun findMessage(): String { + delay(10) + return "Hello World!" + } +} +---- +==== + + Combined with our configuration above, `@PreAuthorize("hasRole('ADMIN')")` will ensure that `findByMessage` is only invoked by a user with the role `ADMIN`. It is important to note that any of the expressions in standard method security work for `@EnableReactiveMethodSecurity`. However, at this time we only support return type of `Boolean` or `boolean` of the expression. @@ -77,7 +164,9 @@ This means that the expression must not block. When integrating with <>, the Reactor Context is automatically established by Spring Security according to the authenticated user. -[source,java] +==== +.Java +[source,java,role="primary"] ---- @EnableWebFluxSecurity @EnableReactiveMethodSecurity @@ -112,4 +201,37 @@ public class SecurityConfig { ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity +class SecurityConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, permitAll) + } + httpBasic { } + } + } + + @Bean + fun userDetailsService(): MapReactiveUserDetailsService { + val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder() + val rob = userBuilder.username("rob") + .password("rob") + .roles("USER") + .build() + val admin = userBuilder.username("admin") + .password("admin") + .roles("USER", "ADMIN") + .build() + return MapReactiveUserDetailsService(rob, admin) + } +} +---- +==== + You can find a complete sample in {gh-samples-url}/javaconfig/hellowebflux-method[hellowebflux-method]