From 07461f418a8511424a4021b4ccc28a4c5808ef47 Mon Sep 17 00:00:00 2001 From: maryarm Date: Sat, 30 Nov 2019 21:09:33 +0200 Subject: [PATCH] BAEL-3338: A Guide to AuthenticationManagerResolver in Spring Security --- spring-5-reactive-security/pom.xml | 1 + .../authresolver/AuthResolverApplication.java | 15 +++ .../authresolver/AuthResolverController.java | 25 +++++ .../AuthResolverSecurityConfig.java | 86 +++++++++++++++++ .../AuthResolverIntegrationTest.java | 62 +++++++++++++ spring-5-security/pom.xml | 5 +- .../authresolver/AuthResolverApplication.java | 11 +++ .../authresolver/AuthResolverController.java | 18 ++++ .../AuthResolverWebSecurityConfigurer.java | 92 +++++++++++++++++++ .../AuthResolverIntegrationTest.java | 80 ++++++++++++++++ 10 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverApplication.java create mode 100644 spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverController.java create mode 100644 spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverSecurityConfig.java create mode 100644 spring-5-reactive-security/src/test/java/com/baeldung/reactive/authresolver/AuthResolverIntegrationTest.java create mode 100644 spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverApplication.java create mode 100644 spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverController.java create mode 100644 spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverWebSecurityConfigurer.java create mode 100644 spring-5-security/src/test/java/com/baeldung/authresolver/AuthResolverIntegrationTest.java diff --git a/spring-5-reactive-security/pom.xml b/spring-5-reactive-security/pom.xml index 72a73a86ce..1a7dba2b9e 100644 --- a/spring-5-reactive-security/pom.xml +++ b/spring-5-reactive-security/pom.xml @@ -129,6 +129,7 @@ 1.0 4.1 3.1.6.RELEASE + 2.2.1.RELEASE diff --git a/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverApplication.java b/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverApplication.java new file mode 100644 index 0000000000..bad5768c20 --- /dev/null +++ b/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.reactive.authresolver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.reactive.config.EnableWebFlux; + +@EnableWebFlux +@SpringBootApplication +public class AuthResolverApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthResolverApplication.class, args); + } + +} diff --git a/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverController.java b/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverController.java new file mode 100644 index 0000000000..fdce66380b --- /dev/null +++ b/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverController.java @@ -0,0 +1,25 @@ +package com.baeldung.reactive.authresolver; + +import java.security.Principal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +public class AuthResolverController { + + @GetMapping("/customer/welcome") + public Mono sayWelcomeToCustomer(Mono principal) { + return principal + .map(Principal::getName) + .map(name -> String.format("Welcome to our site, %s!", name)); + } + + @GetMapping("/employee/welcome") + public Mono sayWelcomeToEmployee(Mono principal) { + return principal + .map(Principal::getName) + .map(name -> String.format("Welcome to our company, %s!", name)); + } + +} diff --git a/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverSecurityConfig.java b/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverSecurityConfig.java new file mode 100644 index 0000000000..65ee47ecc4 --- /dev/null +++ b/spring-5-reactive-security/src/main/java/com/baeldung/reactive/authresolver/AuthResolverSecurityConfig.java @@ -0,0 +1,86 @@ +package com.baeldung.reactive.authresolver; + +import java.util.Collections; +import org.springframework.context.annotation.Bean; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import reactor.core.publisher.Mono; + +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity +public class AuthResolverSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + return http + .authorizeExchange() + .pathMatchers("/**") + .authenticated() + .and() + .httpBasic() + .disable() + .addFilterAfter(authenticationWebFilter(), SecurityWebFiltersOrder.REACTOR_CONTEXT) + .build(); + } + + public AuthenticationWebFilter authenticationWebFilter() { + AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManagerResolver()); + return filter; + } + + public ReactiveAuthenticationManagerResolver authenticationManagerResolver() { + return request -> { + if (request + .getPath() + .subPath(0) + .value() + .startsWith("/employee")) return Mono.just(employeesAuthenticationManager()); + return Mono.just(customersAuthenticationManager()); + }; + } + + public ReactiveAuthenticationManager customersAuthenticationManager() { + return authentication -> customer(authentication) + .switchIfEmpty(Mono.error(new UsernameNotFoundException(authentication + .getPrincipal() + .toString()))) + .map(b -> new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")))); + } + + public ReactiveAuthenticationManager employeesAuthenticationManager() { + return authentication -> employee(authentication) + .switchIfEmpty(Mono.error(new UsernameNotFoundException(authentication + .getPrincipal() + .toString()))) + .map(b -> new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")))); + } + + public Mono customer(Authentication authentication) { + return Mono.justOrEmpty(authentication + .getPrincipal() + .toString() + .startsWith("customer") ? authentication + .getPrincipal() + .toString() : null); + } + + public Mono employee(Authentication authentication) { + return Mono.justOrEmpty(authentication + .getPrincipal() + .toString() + .startsWith("employee") ? authentication + .getPrincipal() + .toString() : null); + } +} diff --git a/spring-5-reactive-security/src/test/java/com/baeldung/reactive/authresolver/AuthResolverIntegrationTest.java b/spring-5-reactive-security/src/test/java/com/baeldung/reactive/authresolver/AuthResolverIntegrationTest.java new file mode 100644 index 0000000000..21c25a6111 --- /dev/null +++ b/spring-5-reactive-security/src/test/java/com/baeldung/reactive/authresolver/AuthResolverIntegrationTest.java @@ -0,0 +1,62 @@ +package com.baeldung.reactive.authresolver; + +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +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.util.Base64Utils; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AuthResolverApplication.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AuthResolverIntegrationTest { + @Autowired private WebTestClient testClient; + + @Test + public void givenCustomerCredential_whenWelcomeCustomer_thenExpectOk() { + testClient + .get() + .uri("/customer/welcome") + .header("Authorization", "Basic " + Base64Utils.encodeToString("customer1:pass1".getBytes())) + .exchange() + .expectStatus() + .isOk(); + } + + @Test + public void givenEmployeeCredential_whenWelcomeCustomer_thenExpect401Status() { + testClient + .get() + .uri("/customer/welcome") + .header("Authorization", "Basic " + Base64Utils.encodeToString("employee1:pass1".getBytes())) + .exchange() + .expectStatus() + .isUnauthorized(); + } + + @Test + public void givenEmployeeCredential_whenWelcomeEmployee_thenExpectOk() { + testClient + .get() + .uri("/employee/welcome") + .header("Authorization", "Basic " + Base64Utils.encodeToString("employee1:pass1".getBytes())) + .exchange() + .expectStatus() + .isOk(); + } + + @Test + public void givenCustomerCredential_whenWelcomeEmployee_thenExpect401Status() { + testClient + .get() + .uri("/employee/welcome") + .header("Authorization", "Basic " + Base64Utils.encodeToString("customer1:pass1".getBytes())) + .exchange() + .expectStatus() + .isUnauthorized(); + } +} diff --git a/spring-5-security/pom.xml b/spring-5-security/pom.xml index 413337633f..943bfbdc1f 100644 --- a/spring-5-security/pom.xml +++ b/spring-5-security/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.baeldung spring-5-security @@ -60,5 +60,8 @@ + + 2.2.1.RELEASE + diff --git a/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverApplication.java b/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverApplication.java new file mode 100644 index 0000000000..96ee674b15 --- /dev/null +++ b/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverApplication.java @@ -0,0 +1,11 @@ +package com.baeldung.authresolver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthResolverApplication { + public static void main(String[] args) { + SpringApplication.run(AuthResolverApplication.class, args); + } +} diff --git a/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverController.java b/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverController.java new file mode 100644 index 0000000000..7dc6900b5a --- /dev/null +++ b/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverController.java @@ -0,0 +1,18 @@ +package com.baeldung.authresolver; + +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AuthResolverController { + @GetMapping("/customer/welcome") + public String sayWelcomeToCustomer(Authentication authentication) { + return String.format("Welcome to our site, %s!", authentication.getPrincipal()); + } + + @GetMapping("/employee/welcome") + public String sayWelcomeToEmployee(Authentication authentication) { + return String.format("Welcome to our company, %s!", authentication.getPrincipal()); + } +} diff --git a/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverWebSecurityConfigurer.java b/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverWebSecurityConfigurer.java new file mode 100644 index 0000000000..6f8931eb7a --- /dev/null +++ b/spring-5-security/src/main/java/com/baeldung/authresolver/AuthResolverWebSecurityConfigurer.java @@ -0,0 +1,92 @@ +package com.baeldung.authresolver; + +import java.util.Arrays; +import javax.servlet.http.HttpServletRequest; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.AuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationConverter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +@Configuration +public class AuthResolverWebSecurityConfigurer extends WebSecurityConfigurerAdapter { + + public AuthenticationConverter authenticationConverter() { + return new BasicAuthenticationConverter(); + } + + public AuthenticationManagerResolver authenticationManagerResolver() { + return request -> { + if (request + .getPathInfo() + .startsWith("/employee")) return employeesAuthenticationManager(); + return customersAuthenticationManager(); + }; + } + + public AuthenticationManager customersAuthenticationManager() { + return authentication -> { + if (isCustomer(authentication)) { + return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))); + } + throw new UsernameNotFoundException(authentication + .getPrincipal() + .toString()); + }; + } + + public boolean isCustomer(Authentication authentication) { + return (authentication + .getPrincipal() + .toString() + .startsWith("customer")); + } + + public boolean isEmployee(Authentication authentication) { + return (authentication + .getPrincipal() + .toString() + .startsWith("employee")); + } + + public AuthenticationFilter authenticationFilter(AuthenticationManagerResolver resolver, AuthenticationConverter converter) { + AuthenticationFilter ret = new AuthenticationFilter(resolver, converter); + ret.setSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> { + }); + return ret; + } + + public AuthenticationManager employeesAuthenticationManager() { + return authentication -> { + if (isEmployee(authentication)) { + return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))); + } + throw new UsernameNotFoundException(authentication + .getPrincipal() + .toString()); + }; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .addFilterBefore( + authenticationFilter( + authenticationManagerResolver(), authenticationConverter()), + BasicAuthenticationFilter.class); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + super.configure(auth); + } +} diff --git a/spring-5-security/src/test/java/com/baeldung/authresolver/AuthResolverIntegrationTest.java b/spring-5-security/src/test/java/com/baeldung/authresolver/AuthResolverIntegrationTest.java new file mode 100644 index 0000000000..0b0289e9e5 --- /dev/null +++ b/spring-5-security/src/test/java/com/baeldung/authresolver/AuthResolverIntegrationTest.java @@ -0,0 +1,80 @@ +package com.baeldung.authresolver; + +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.Base64Utils; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AuthResolverApplication.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AuthResolverIntegrationTest { + + @Autowired private FilterChainProxy springSecurityFilterChain; + + @Autowired private WebApplicationContext wac; + + private MockMvc mockMvc; + + @Before + public void setup() { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(wac) + .apply(springSecurity(springSecurityFilterChain)) + .build(); + } + + @Test + public void givenCustomerCredential_whenWelcomeCustomer_thenExpectOk() throws Exception { + this.mockMvc + .perform(get("/customer/welcome") + .header( + "Authorization", String.format("Basic %s", Base64Utils.encodeToString("customer1:pass1".getBytes())) + ) + ) + .andExpect(status().is2xxSuccessful()); + } + + @Test + public void givenEmployeeCredential_whenWelcomeCustomer_thenExpect401Status() throws Exception { + this.mockMvc + .perform(get("/customer/welcome") + .header( + "Authorization", "Basic " + Base64Utils.encodeToString("employee1:pass1".getBytes())) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + public void givenEmployeeCredential_whenWelcomeEmployee_thenExpectOk() throws Exception { + this.mockMvc + .perform(get("/employee/welcome") + .header( + "Authorization", "Basic " + Base64Utils.encodeToString("employee1:pass1".getBytes())) + ) + .andExpect(status().is2xxSuccessful()); + } + + @Test + public void givenCustomerCredential_whenWelcomeEmployee_thenExpect401Status() throws Exception { + this.mockMvc + .perform(get("/employee/welcome") + .header( + "Authorization", "Basic " + Base64Utils.encodeToString("customer1:pass1".getBytes())) + ) + .andExpect(status().isUnauthorized()); + } +}