1
0
mirror of synced 2026-08-06 02:08:01 +00:00

Update WebFlux samples to use Spring Boot

Fixes: gh-5411
This commit is contained in:
Rob Winch
2018-06-05 10:27:08 -05:00
parent 1ee3c765f6
commit a9e6d7606e
39 changed files with 305 additions and 555 deletions
@@ -0,0 +1,12 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-config')
compile project(':spring-security-web')
compile 'org.springframework.boot:spring-boot-starter-webflux'
testCompile project(':spring-security-test')
testCompile 'io.projectreactor:reactor-test'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
@@ -0,0 +1,87 @@
/*
* Copyright 2002-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;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
import java.util.Map;
import java.util.function.Consumer;
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.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloWebfluxMethodApplicationITests {
WebTestClient rest;
@Autowired
public void setRest(WebTestClient rest) {
this.rest = rest
.mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication()));
}
@Test
public void messageWhenNotAuthenticated() throws Exception {
this.rest
.get()
.uri("/message")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
public void messageWhenUserThenForbidden() throws Exception {
this.rest
.get()
.uri("/message")
.attributes(robsCredentials())
.exchange()
.expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
public void messageWhenAdminThenOk() throws Exception {
this.rest
.get()
.uri("/message")
.attributes(adminCredentials())
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World!");
}
private Consumer<Map<String, Object>> robsCredentials() {
return basicAuthenticationCredentials("rob", "rob");
}
private Consumer<Map<String, Object>> adminCredentials() {
return basicAuthenticationCredentials("admin", "admin");
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Rob Winch
* @since 5.0
*/
@SpringBootApplication
public class HelloWebfluxMethodApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWebfluxMethodApplication.class, args);
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2002-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;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* @author Rob Winch
* @since 5.0
*/
@Component
public class HelloWorldMessageService {
@PreAuthorize("hasRole('ADMIN')")
public Mono<String> findMessage() {
return Mono.just("Hello World!");
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2002-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;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
/**
* @author Rob Winch
* @since 5.0
*/
@RestController
public class MessageController {
private final HelloWorldMessageService messages;
public MessageController(HelloWorldMessageService messages) {
this.messages = messages;
}
@GetMapping("/message")
public Mono<String> message() {
return this.messages.findMessage();
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2002-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;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* @author Rob Winch
* @since 5.0
*/
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
return http
// Demonstrate that method security works
// Best practice to use both for defense in depth
.authorizeExchange()
.anyExchange().permitAll()
.and()
.httpBasic().and()
.build();
}
@Bean
public MapReactiveUserDetailsService userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
return new MapReactiveUserDetailsService(rob, admin);
}
}
@@ -0,0 +1,140 @@
/*
* Copyright 2002-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;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
import java.util.Map;
import java.util.function.Consumer;
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.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloWebfluxMethodApplicationTests {
WebTestClient rest;
@Autowired
public void setup(ApplicationContext context) {
this.rest = WebTestClient
.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.filter(basicAuthentication())
.build();
}
@Test
public void messageWhenNotAuthenticated() throws Exception {
this.rest
.get()
.uri("/message")
.exchange()
.expectStatus().isUnauthorized();
}
// --- Basic Authentication ---
@Test
public void messageWhenUserThenForbidden() throws Exception {
this.rest
.get()
.uri("/message")
.attributes(robsCredentials())
.exchange()
.expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
public void messageWhenAdminThenOk() throws Exception {
this.rest
.get()
.uri("/message")
.attributes(adminCredentials())
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World!");
}
// --- WithMockUser ---
@Test
@WithMockUser
public void messageWhenWithMockUserThenForbidden() throws Exception {
this.rest
.get()
.uri("/message")
.exchange()
.expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
@WithMockUser(roles = "ADMIN")
public void messageWhenWithMockAdminThenOk() throws Exception {
this.rest
.get()
.uri("/message")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World!");
}
// --- mutateWith mockUser ---
@Test
public void messageWhenMutateWithMockUserThenForbidden() throws Exception {
this.rest
.mutateWith(mockUser())
.get()
.uri("/message")
.exchange()
.expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
public void messageWhenMutateWithMockAdminThenOk() throws Exception {
this.rest
.mutateWith(mockUser().roles("ADMIN"))
.get()
.uri("/message")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World!");
}
private Consumer<Map<String, Object>> robsCredentials() {
return basicAuthenticationCredentials("rob", "rob");
}
private Consumer<Map<String, Object>> adminCredentials() {
return basicAuthenticationCredentials("admin", "admin");
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2002-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;
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.security.access.AccessDeniedException;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import reactor.test.StepVerifier;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloWorldMessageServiceTests {
@Autowired
HelloWorldMessageService messages;
@Test
public void messagesWhenNotAuthenticatedThenDenied() {
StepVerifier.create(this.messages.findMessage())
.expectError(AccessDeniedException.class)
.verify();
}
@Test
@WithMockUser
public void messagesWhenUserThenDenied() {
StepVerifier.create(this.messages.findMessage())
.expectError(AccessDeniedException.class)
.verify();
}
@Test
@WithMockUser(roles = "ADMIN")
public void messagesWhenAdminThenOk() {
StepVerifier.create(this.messages.findMessage())
.expectNext("Hello World!")
.verifyComplete();
}
}
@@ -0,0 +1,11 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-config')
compile project(':spring-security-web')
compile 'org.springframework.boot:spring-boot-starter-webflux'
testCompile project(':spring-security-test')
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
@@ -0,0 +1,85 @@
/*
* Copyright 2002-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;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
import java.util.Map;
import java.util.function.Consumer;
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.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloWebfluxApplicationITests {
WebTestClient rest;
@Autowired
public void setRest(WebTestClient rest) {
this.rest = rest
.mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication()));
}
@Test
public void basicWhenNoCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
public void basicWhenValidCredentialsThenOk() throws Exception {
this.rest
.get()
.uri("/")
.attributes(userCredentials())
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
@Test
public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.attributes(invalidCredentials())
.exchange()
.expectStatus().isUnauthorized()
.expectBody().isEmpty();
}
private Consumer<Map<String, Object>> userCredentials() {
return basicAuthenticationCredentials("user", "user");
}
private Consumer<Map<String, Object>> invalidCredentials() {
return basicAuthenticationCredentials("user", "INVALID");
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2002-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;
import java.security.Principal;
import java.util.Collections;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
/**
* @author Rob Winch
* @since 5.0
*/
@RestController
public class HelloUserController {
@GetMapping("/")
public Mono<Map<String, String>> hello(Mono<Principal> principal) {
return principal
.map(Principal::getName)
.map(this::helloMessage);
}
private Map<String, String> helloMessage(String username) {
return Collections.singletonMap("message", "Hello " + username + "!");
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2002-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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Rob Winch
* @since 5.0
*/
@SpringBootApplication
public class HelloWebfluxApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWebfluxApplication.class, args);
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2002-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;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
/**
* @author Rob Winch
* @since 5.0
*/
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
}
@@ -0,0 +1,116 @@
/*
* Copyright 2002-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;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWebTestClient
public class HelloWebfluxApplicationTests {
WebTestClient rest;
@Autowired
public void setup(ApplicationContext context) {
this.rest = WebTestClient
.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.filter(basicAuthentication())
.build();
}
@Test
public void basicWhenNoCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
public void basicWhenValidCredentialsThenOk() throws Exception {
this.rest
.get()
.uri("/")
.attributes(userCredentials())
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
@Test
public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.attributes(invalidCredentials())
.exchange()
.expectStatus().isUnauthorized()
.expectBody().isEmpty();
}
@Test
public void mockSupportWhenMutateWithMockUserThenOk() throws Exception {
this.rest
.mutateWith(mockUser())
.get()
.uri("/")
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
@Test
@WithMockUser
public void mockSupportWhenWithMockUserThenOk() throws Exception {
this.rest
.get()
.uri("/")
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
private Consumer<Map<String, Object>> userCredentials() {
return basicAuthenticationCredentials("user", "user");
}
private Consumer<Map<String, Object>> invalidCredentials() {
return basicAuthenticationCredentials("user", "INVALID");
}
}
@@ -0,0 +1,11 @@
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-config')
compile project(':spring-security-web')
compile 'org.springframework.boot:spring-boot-starter-webflux'
testCompile project(':spring-security-test')
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
@@ -0,0 +1,85 @@
/*
* Copyright 2002-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;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
import java.util.Map;
import java.util.function.Consumer;
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.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloWebfluxFnApplicationITests {
WebTestClient rest;
@Autowired
public void setRest(WebTestClient rest) {
this.rest = rest
.mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication()));
}
@Test
public void basicWhenNoCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
public void basicWhenValidCredentialsThenOk() throws Exception {
this.rest
.get()
.uri("/")
.attributes(userCredentials())
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
@Test
public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.attributes(invalidCredentials())
.exchange()
.expectStatus().isUnauthorized()
.expectBody().isEmpty();
}
private Consumer<Map<String, Object>> userCredentials() {
return basicAuthenticationCredentials("user", "user");
}
private Consumer<Map<String, Object>> invalidCredentials() {
return basicAuthenticationCredentials("user", "INVALID");
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2002-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;
import java.security.Principal;
import java.util.Collections;
import reactor.core.publisher.Mono;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
/**
* @author Rob Winch
* @since 5.0
*/
@Component
public class HelloUserController {
public Mono<ServerResponse> hello(ServerRequest serverRequest) {
return serverRequest.principal()
.map(Principal::getName)
.flatMap(username ->
ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.syncBody(Collections.singletonMap("message", "Hello " + username + "!"))
);
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2002-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;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
/**
* @author Rob Winch
* @since 5.0
*/
@SpringBootApplication
public class HelloWebfluxFnApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWebfluxFnApplication.class, args);
}
@Bean
public RouterFunction<ServerResponse> routes(HelloUserController userController) {
return route(
GET("/"), userController::hello);
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2002-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;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
/**
* @author Rob Winch
* @since 5.0
*/
@EnableWebFluxSecurity
public class HelloWebfluxFnSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
}
@@ -0,0 +1,117 @@
/*
* Copyright 2002-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;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWebTestClient
public class HelloWebfluxFnApplicationTests {
WebTestClient rest;
@Autowired
public void setup(ApplicationContext context) {
this.rest = WebTestClient
.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.filter(basicAuthentication())
.build();
}
@Test
public void basicWhenNoCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
public void basicWhenValidCredentialsThenOk() throws Exception {
this.rest
.get()
.uri("/")
.attributes(userCredentials())
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
@Test
public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception {
this.rest
.get()
.uri("/")
.attributes(invalidCredentials())
.exchange()
.expectStatus().isUnauthorized()
.expectBody().isEmpty();
}
@Test
public void mockSupportWhenMutateWithMockUserThenOk() throws Exception {
this.rest
.mutateWith(mockUser())
.get()
.uri("/")
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
@Test
@WithMockUser
public void mockSupportWhenWithMockUserThenOk() throws Exception {
this.rest
.get()
.uri("/")
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"message\":\"Hello user!\"}");
}
private Consumer<Map<String, Object>> userCredentials() {
return basicAuthenticationCredentials("user", "user");
}
private Consumer<Map<String, Object>> invalidCredentials() {
return basicAuthenticationCredentials("user", "INVALID");
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-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.
*/
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-config')
compile project(':spring-security-web')
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-webflux'
testCompile project(':spring-security-test')
testCompile 'org.springframework.boot:spring-boot-starter-test'
testCompile 'io.projectreactor:reactor-test'
testCompile 'org.skyscreamer:jsonassert'
testCompile 'org.springframework:spring-test'
integrationTestCompile seleniumDependencies
}
@@ -0,0 +1,79 @@
/*
* Copyright 2002-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;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import sample.webdriver.IndexPage;
import sample.webdriver.LoginPage;
/**
* @author Rob Winch
* @since 5.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebfluxFormApplicationTests {
WebDriver driver;
@LocalServerPort
int port;
@Before
public void setup() {
this.driver = new HtmlUnitDriver(BrowserVersion.CHROME);
}
@Test
public void loginWhenInvalidUsernameThenError() throws Exception {
LoginPage login = IndexPage.to(this.driver, this.port, LoginPage.class);
login.assertAt();
login
.loginForm()
.username("invalid")
.password("password")
.submit(LoginPage.class)
.assertError();
}
@Test
public void loginAndLogout() throws Exception {
LoginPage login = IndexPage.to(this.driver, this.port, LoginPage.class);
login.assertAt();
IndexPage index = login
.loginForm()
.username("user")
.password("password")
.submit(IndexPage.class);
index.assertAt();
login = index.logout();
login
.assertAt()
.assertLogout();
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2002-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.webdriver;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @since 5.0
*/
public class IndexPage {
private WebDriver driver;
private WebElement logout;
public IndexPage(WebDriver webDriver) {
this.driver = webDriver;
}
public static <T> T to(WebDriver driver, int port, Class<T> page) {
driver.get("http://localhost:" + port +"/");
return (T) PageFactory.initElements(driver, page);
}
public IndexPage assertAt() {
assertThat(this.driver.getTitle()).isEqualTo("Secured");
return this;
}
public LoginPage logout() {
this.logout.click();
return LoginPage.create(this.driver);
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2002-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.webdriver;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
* @since 5.0
*/
public class LoginPage {
private WebDriver driver;
@FindBy(css = "div[role=alert]")
private WebElement alert;
private LoginForm loginForm;
public LoginPage(WebDriver webDriver) {
this.driver = webDriver;
this.loginForm = PageFactory.initElements(webDriver, LoginForm.class);
}
static LoginPage create(WebDriver driver) {
return PageFactory.initElements(driver, LoginPage.class);
}
public LoginPage assertAt() {
assertThat(this.driver.getTitle()).isEqualTo("Please Log In");
return this;
}
public LoginPage assertError() {
assertThat(this.alert.getText()).isEqualTo("Invalid username and password.");
return this;
}
public LoginPage assertLogout() {
assertThat(this.alert.getText()).isEqualTo("You have been logged out.");
return this;
}
public LoginForm loginForm() {
return this.loginForm;
}
public static class LoginForm {
private WebDriver driver;
private WebElement username;
private WebElement password;
@FindBy(css = "button[type=submit]")
private WebElement submit;
public LoginForm(WebDriver driver) {
this.driver = driver;
}
public LoginForm username(String username) {
this.username.sendKeys(username);
return this;
}
public LoginForm password(String password) {
this.password.sendKeys(password);
return this;
}
public <T> T submit(Class<T> page) {
this.submit.click();
return PageFactory.initElements(this.driver, page);
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2002-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;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import static org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME;
/**
* @author Rob Winch
* @since 5.0
*/
@ControllerAdvice
public class CsrfControllerAdvice {
@ModelAttribute
public Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
return csrfToken.doOnSuccess(token -> exchange.getAttributes().put(DEFAULT_CSRF_ATTR_NAME, token));
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2002-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;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author Rob Winch
* @since 5.0
*/
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/login")
public String login() {
return "login";
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Rob Winch
* @since 5.0
*/
@SpringBootApplication
public class WebfluxFormApplication {
public static void main(String[] args) {
SpringApplication.run(WebfluxFormApplication.class, args);
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2002-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;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* @author Rob Winch
* @since 5.0
*/
@EnableWebFluxSecurity
public class WebfluxFormSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.httpBasic().and()
.formLogin()
.loginPage("/login");
return http.build();
}
}
@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>Secured</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
<h1>Secured</h1>
<form th:action="@{/logout}" method="post">
<input id="logout" type="submit" value="Log Out"/>
</form>
</div>
</body>
</html>
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please Log In</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="http://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form class="form-signin" method="post" th:action="@{/login}">
<h2 class="form-signin-heading">Please Log In</h2>
<div th:if="${param.error}" class="alert alert-danger" role="alert">Invalid
username and password.</div>
<div th:if="${param.logout}" class="alert alert-success" role="alert">You
have been logged out.</div>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</body>
</html>