Add auto-config and starter for reactive security

Closes gh-9925
pull/9737/head
Madhura Bhave 7 years ago
parent 1e11f80181
commit 5d05347e61

@ -527,6 +527,11 @@
<artifactId>spring-security-data</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>

@ -0,0 +1,64 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.autoconfigure.security.reactive;
import java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.userdetails.MapUserDetailsRepository;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsRepository;
/**
* Default user {@link Configuration} for a reactive web application.
* Configures a {@link UserDetailsRepository} with a default user and generated password.
* This backs-off completely if there is a bean of type {@link UserDetailsRepository}
* or {@link ReactiveAuthenticationManager}.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@Configuration
@ConditionalOnClass({ReactiveAuthenticationManager.class})
@ConditionalOnMissingBean({ReactiveAuthenticationManager.class, UserDetailsRepository.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class ReactiveAuthenticationManagerConfiguration {
private static final Log logger = LogFactory
.getLog(ReactiveAuthenticationManagerConfiguration.class);
@Bean
public MapUserDetailsRepository userDetailsRepository() {
String password = UUID.randomUUID().toString();
logger.info(
String.format("%n%nUsing default security password: %s%n", password));
UserDetails user = User.withUsername("user")
.password(password)
.roles()
.build();
return new MapUserDetailsRepository(user);
}
}

@ -0,0 +1,41 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.autoconfigure.security.reactive;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security in a
* reactive application. This auto-configuration adds {@link EnableWebFluxSecurity}
* and delegates to Spring Security's content-negotiation mechanism for authentication.
* In a webapp this configuration also secures all web endpoints.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@Configuration
@ConditionalOnClass({EnableWebFluxSecurity.class, AuthenticationPrincipalArgumentResolver.class})
@Import({ WebfluxSecurityConfiguration.class,
ReactiveAuthenticationManagerConfiguration.class })
public class ReactiveSecurityAutoConfiguration {
}

@ -0,0 +1,38 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.autoconfigure.security.reactive;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.annotation.web.reactive.WebFluxSecurityConfiguration;
/**
* Switches on {@link EnableWebFluxSecurity} for a reactive web application
* if this annotation has not been added by the user.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@ConditionalOnClass(EnableWebFluxSecurity.class)
@ConditionalOnMissingBean(WebFluxSecurityConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@EnableWebFluxSecurity
public class WebfluxSecurityConfiguration {
}

@ -99,6 +99,7 @@ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\
org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration,\

@ -0,0 +1,139 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.autoconfigure.security.reactive;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.reactive.MockReactiveWebServerFactory;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.HttpSecurityConfiguration;
import org.springframework.security.config.annotation.web.reactive.WebFluxSecurityConfiguration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.MapUserDetailsRepository;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsRepository;
import org.springframework.security.web.server.WebFilterChainFilter;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ReactiveSecurityAutoConfiguration}.
*
* @author Madhura Bhave
*/
public class ReactiveSecurityAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner(ReactiveWebServerApplicationContext::new);
@Test
public void enablesWebFluxSecurity() {
this.contextRunner.withUserConfiguration(TestConfig.class)
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class))
.run(context -> {
assertThat(context).getBean(HttpSecurityConfiguration.class).isNotNull();
assertThat(context).getBean(WebFluxSecurityConfiguration.class).isNotNull();
assertThat(context).getBean(WebFilterChainFilter.class).isNotNull();
});
}
@Test
public void configuresADefaultUser() {
this.contextRunner.withUserConfiguration(TestConfig.class)
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class))
.run(context -> {
UserDetailsRepository userDetailsRepository = context.getBean(UserDetailsRepository.class);
assertThat(userDetailsRepository.findByUsername("user").block()).isNotNull();
});
}
@Test
public void doesNotConfigureDefaultUserIfUserDetailsRepositoryAvailable() {
this.contextRunner.withUserConfiguration(UserConfig.class, TestConfig.class)
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class))
.run(context -> {
UserDetailsRepository userDetailsRepository = context.getBean(UserDetailsRepository.class);
assertThat(userDetailsRepository.findByUsername("user").block()).isNull();
assertThat(userDetailsRepository.findByUsername("foo").block()).isNotNull();
assertThat(userDetailsRepository.findByUsername("admin").block()).isNotNull();
});
}
@Test
public void doesNotConfigureDefaultUserIfAuthenticationManagerAvailable() {
this.contextRunner.withUserConfiguration(AuthenticationManagerConfig.class, TestConfig.class)
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class))
.run(context -> {
assertThat(context).getBean(UserDetailsRepository.class).isNull();
});
}
@Configuration
@EnableWebFlux
static class TestConfig {
@Bean
public HttpHandler httpHandler(ApplicationContext applicationContext) {
return WebHttpHandlerBuilder.applicationContext(applicationContext).build();
}
@Bean
public ReactiveWebServerFactory reactiveWebServerFactory() {
return new MockReactiveWebServerFactory();
}
}
@Configuration
static class UserConfig {
@Bean
public MapUserDetailsRepository userDetailsRepository() {
UserDetails foo = User.withUsername("foo").password("foo").roles("USER").build();
UserDetails admin = User.withUsername("admin").password("admin").roles("USER", "ADMIN").build();
return new MapUserDetailsRepository(foo, admin);
}
}
@Configuration
static class AuthenticationManagerConfig {
@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
return new ReactiveAuthenticationManager() {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return null;
}
};
}
}
}

@ -506,6 +506,11 @@
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-reactive</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-social-facebook</artifactId>

@ -67,6 +67,7 @@
<module>spring-boot-sample-property-validation</module>
<module>spring-boot-sample-quartz</module>
<module>spring-boot-sample-secure</module>
<module>spring-boot-sample-secure-webflux</module>
<module>spring-boot-sample-servlet</module>
<module>spring-boot-sample-session</module>
<module>spring-boot-sample-simple</module>

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<!-- Your own application should inherit from spring-boot-starter-parent -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-samples</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-sample-secure-webflux</artifactId>
<name>Spring Boot Secure WebFlux Sample</name>
<description>Spring Boot Secure WebFlux Sample</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
<m2eclipse.wtp.contextRoot>/</m2eclipse.wtp.contextRoot>
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-reactive</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>generate build info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,32 @@
/*
* Copyright 2012-2017 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
*
* http://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 sample.secure.webflux;
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
@Component
public class EchoHandler {
public Mono<ServerResponse> echo(ServerRequest request) {
return ServerResponse.ok().body(request.bodyToMono(String.class), String.class);
}
}

@ -0,0 +1,50 @@
/*
* Copyright 2012-2017 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
*
* http://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 sample.secure.webflux;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.MapUserDetailsRepository;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsRepository;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@SpringBootApplication
public class SampleSecureWebFluxApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleSecureWebFluxApplication.class);
}
@Bean
public RouterFunction<ServerResponse> monoRouterFunction(EchoHandler echoHandler) {
return route(POST("/echo"), echoHandler::echo);
}
@Bean
public UserDetailsRepository userDetailsRepository() {
UserDetails user = User.withUsername("foo").password("password").roles("USER").build();
return new MapUserDetailsRepository(user);
}
}

@ -0,0 +1,32 @@
/*
* Copyright 2012-2017 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
*
* http://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 sample.secure.webflux;
import java.security.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WelcomeController {
@GetMapping("/")
public String welcome(Principal principal) {
return "Hello " + principal.getName();
}
}

@ -0,0 +1,77 @@
/*
* Copyright 2012-2017 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
*
* http://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 sample.secure.webflux;
import java.util.Base64;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration tests for a secure reactive application.
*
* @author Madhura Bhave
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class SampleSecureWebFluxApplicationTests {
@Autowired
private WebTestClient webClient;
@Test
public void userDefinedMappingsSecureByDefault() {
this.webClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void actuatorsSecureByDefault() {
this.webClient.get().uri("/application/status").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isUnauthorized();
}
@Test
public void userDefinedMappingsAccessibleOnLogin() {
this.webClient.get().uri("/").accept(MediaType.APPLICATION_JSON)
.header("Authorization", "basic " + getBasicAuth())
.exchange()
.expectBody(String.class).isEqualTo("Hello foo");
}
@Test
public void actuatorsAccessibleOnLogin() {
this.webClient.get().uri("/application/status").accept(MediaType.APPLICATION_JSON)
.header("Authorization", "basic " + getBasicAuth())
.exchange()
.expectBody(String.class).isEqualTo("{\"status\":\"UP\"}");
}
private String getBasicAuth() {
return new String(Base64.getEncoder().encode(("foo:password").getBytes()));
}
}

@ -63,6 +63,7 @@
<module>spring-boot-starter-quartz</module>
<module>spring-boot-starter-reactor-netty</module>
<module>spring-boot-starter-security</module>
<module>spring-boot-starter-security-reactive</module>
<module>spring-boot-starter-social-facebook</module>
<module>spring-boot-starter-social-twitter</module>
<module>spring-boot-starter-social-linkedin</module>

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-starter-security-reactive</artifactId>
<name>Spring Boot Security Reactive Starter</name>
<description>Starter for using reactive Spring Security</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<exclusions>
<exclusion>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-webflux</artifactId>
<exclusions>
<exclusion>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
Loading…
Cancel
Save