Add Max Sessions on WebFlux Sample

This commit is contained in:
Marcus Hert Da Coregio
2023-11-24 10:40:04 -03:00
parent d8748849dc
commit c09de87362
15 changed files with 762 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2023 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 com.example.maximumsessions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class HelloController {
@GetMapping("/hello")
String hello() {
return "Hello!";
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2023 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 com.example.maximumsessions;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MaximumSessionsApplication {
public static void main(String[] args) {
SpringApplication.run(MaximumSessionsApplication.class, args);
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2023 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 com.example.maximumsessions;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.PreventLoginServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.SessionLimit;
import org.springframework.security.web.session.WebSessionStoreReactiveSessionRegistry;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
public class SecurityConfig {
@Value("${prevent-login:false}")
private boolean preventLogin;
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
.formLogin(Customizer.withDefaults())
.sessionManagement((sessions) -> sessions
.concurrentSessions((concurrency) -> concurrency
.maximumSessions(maxSessions())
.maximumSessionsExceededHandler(maximumSessionsExceededHandler()))
);
return http.build();
// @formatter:on
}
@Bean
ReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder().username("admin").password("password").roles("ADMIN")
.build();
UserDetails unlimitedSessions = User.withDefaultPasswordEncoder().username("unlimited").password("password")
.roles("UNLIMITED_SESSIONS").build();
return new MapReactiveUserDetailsService(user, admin, unlimitedSessions);
}
@Bean
ReactiveSessionRegistry reactiveSessionRegistry(WebSessionManager webSessionManager) {
return new WebSessionStoreReactiveSessionRegistry(
((DefaultWebSessionManager) webSessionManager).getSessionStore());
}
private SessionLimit maxSessions() {
return (authentication) -> {
if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_UNLIMITED_SESSIONS"))) {
return Mono.just(-1);
}
if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
return Mono.just(2);
}
return Mono.just(1);
};
}
private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler() {
if (this.preventLogin) {
return new PreventLoginServerMaximumSessionsExceededHandler();
}
return new InvalidateLeastUsedServerMaximumSessionsExceededHandler();
}
}

View File

@@ -0,0 +1,89 @@
package com.example.maximumsessions;
import org.junit.jupiter.api.Test;
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.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@TestPropertySource(properties = "prevent-login=true")
class MaximumSessionsApplicationPreventLoginTests {
@Autowired
WebTestClient client;
@Test
void loginWhenUserAndMaximumSessionsOf1ExceededThenSecondLoginProhibited() {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
ResponseCookie firstLoginCookie = loginReturningCookie(data);
login(data).expectStatus().isFound().expectHeader().location("/login?error");
performHello(firstLoginCookie).expectStatus().isOk();
}
@Test
void loginWhenAdminAndMaximumSessionsOf2ExceededThenThirdLoginProhibited() {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "admin");
data.add("password", "password");
ResponseCookie firstLoginCookie = loginReturningCookie(data);
ResponseCookie secondLoginCookie = loginReturningCookie(data);
// update last access time for first login session
performHello(firstLoginCookie).expectStatus().isOk();
login(data).expectStatus().isFound().expectHeader().location("/login?error");
performHello(firstLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(secondLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
}
@Test
void loginWhenAuthenticationHasUnlimitedSessionsThenLoginIsAlwaysAllowed() {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "unlimited");
data.add("password", "password");
ResponseCookie firstLoginCookie = loginReturningCookie(data);
ResponseCookie secondLoginCookie = loginReturningCookie(data);
ResponseCookie thirdLoginCookie = loginReturningCookie(data);
ResponseCookie fourthLoginCookie = loginReturningCookie(data);
ResponseCookie fifthLoginCookie = loginReturningCookie(data);
performHello(firstLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(secondLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(thirdLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(fourthLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(fifthLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
}
private WebTestClient.ResponseSpec performHello(ResponseCookie cookie) {
return this.client.get().uri("/hello").cookie(cookie.getName(), cookie.getValue()).exchange();
}
private ResponseCookie loginReturningCookie(MultiValueMap<String, String> data) {
return login(data).expectCookie().exists("SESSION").returnResult(Void.class).getResponseCookies()
.getFirst("SESSION");
}
private WebTestClient.ResponseSpec login(MultiValueMap<String, String> data) {
return this.client.mutateWith(csrf()).post().uri("/login").contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromFormData(data)).exchange();
}
}

View File

@@ -0,0 +1,90 @@
package com.example.maximumsessions;
import org.junit.jupiter.api.Test;
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.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class MaximumSessionsApplicationTests {
@Autowired
WebTestClient client;
@Test
void loginWhenUserAndMaximumSessionsOf1ExceededThenLeastUsedInvalidated() {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "user");
data.add("password", "password");
ResponseCookie firstLoginCookie = loginReturningCookie(data);
ResponseCookie secondLoginCookie = loginReturningCookie(data);
performHello(firstLoginCookie).expectStatus().isFound().expectHeader().location("/login");
performHello(secondLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
}
@Test
void loginWhenAdminAndMaximumSessionsOf2ExceededThenLeastUsedInvalidated() {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "admin");
data.add("password", "password");
ResponseCookie firstLoginCookie = loginReturningCookie(data);
ResponseCookie secondLoginCookie = loginReturningCookie(data);
// update last access time for first login session
performHello(firstLoginCookie).expectStatus().isOk();
ResponseCookie thirdLoginCookie = loginReturningCookie(data);
performHello(firstLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(secondLoginCookie).expectStatus().isFound().expectHeader().location("/login");
performHello(thirdLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
}
@Test
void loginWhenAuthenticationHasUnlimitedSessionsThenSessionsAreValid() {
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("username", "unlimited");
data.add("password", "password");
ResponseCookie firstLoginCookie = loginReturningCookie(data);
ResponseCookie secondLoginCookie = loginReturningCookie(data);
ResponseCookie thirdLoginCookie = loginReturningCookie(data);
ResponseCookie fourthLoginCookie = loginReturningCookie(data);
ResponseCookie fifthLoginCookie = loginReturningCookie(data);
performHello(firstLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(secondLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(thirdLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(fourthLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
performHello(fifthLoginCookie).expectStatus().isOk().expectBody(String.class).isEqualTo("Hello!");
}
private WebTestClient.ResponseSpec performHello(ResponseCookie cookie) {
return this.client.get().uri("/hello").cookie(cookie.getName(), cookie.getValue()).exchange();
}
private ResponseCookie loginReturningCookie(MultiValueMap<String, String> data) {
return login(data).expectCookie().exists("SESSION").returnResult(Void.class).getResponseCookies()
.getFirst("SESSION");
}
private WebTestClient.ResponseSpec login(MultiValueMap<String, String> data) {
return this.client.mutateWith(csrf()).post().uri("/login").contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromFormData(data)).exchange();
}
}