2
0
Эх сурвалжийг харах

Add LogoutWebFilter

Fixes gh-4539
Rob Winch 8 жил өмнө
parent
commit
e14af37775

+ 2 - 0
config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java

@@ -24,6 +24,7 @@ import java.util.List;
 import org.springframework.http.MediaType;
 import org.springframework.security.web.server.DelegatingAuthenticationEntryPoint;
 import org.springframework.security.web.server.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.server.authentication.logout.LogoutWebFiter;
 import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
 import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
 import reactor.core.publisher.Mono;
@@ -175,6 +176,7 @@ public class HttpSecurity {
 				filters.add(new LoginPageGeneratingWebFilter());
 			}
 			filters.add(this.formLogin.build());
+			filters.add(new LogoutWebFiter());
 		}
 		filters.add(new AuthenticationReactorContextFilter());
 		if(this.authorizeExchangeBuilder != null) {

+ 35 - 1
config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java

@@ -67,7 +67,14 @@ public class FormLoginTests {
 			.webTestClientSetup(webTestClient)
 			.build();
 
-		DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class);
+		DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class)
+			.assertAt();
+
+		loginPage = loginPage.loginForm()
+			.username("user")
+			.password("invalid")
+			.submit(DefaultLoginPage.class)
+			.assertError();
 
 		HomePage homePage = loginPage.loginForm()
 			.username("user")
@@ -75,6 +82,12 @@ public class FormLoginTests {
 			.submit(HomePage.class);
 
 		homePage.assertAt();
+
+		driver.get("http://localhost/logout");
+
+		DefaultLoginPage.create(driver)
+			.assertAt()
+			.assertLogout();
 	}
 
 	@Test
@@ -161,6 +174,8 @@ public class FormLoginTests {
 	public static class DefaultLoginPage {
 
 		private WebDriver driver;
+		@FindBy(css = "div[role=alert]")
+		private WebElement alert;
 
 		private LoginForm loginForm;
 
@@ -169,6 +184,25 @@ public class FormLoginTests {
 			this.loginForm = PageFactory.initElements(webDriver, LoginForm.class);
 		}
 
+		static DefaultLoginPage create(WebDriver driver) {
+			return PageFactory.initElements(driver, DefaultLoginPage.class);
+		}
+
+		public DefaultLoginPage assertAt() {
+			assertThat(this.driver.getTitle()).isEqualTo("Please sign in");
+			return this;
+		}
+
+		public DefaultLoginPage assertError() {
+			assertThat(this.alert.getText()).isEqualTo("Invalid credentials");
+			return this;
+		}
+
+		public DefaultLoginPage assertLogout() {
+			assertThat(this.alert.getText()).isEqualTo("You have been signed out");
+			return this;
+		}
+
 		public LoginForm loginForm() {
 			return this.loginForm;
 		}

+ 31 - 0
webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutHandler.java

@@ -0,0 +1,31 @@
+/*
+ *
+ *  * 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 org.springframework.security.web.server.authentication.logout;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.WebFilterExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public interface LogoutHandler {
+	Mono<Void> logout(WebFilterExchange exchange, Authentication authentication);
+}

+ 54 - 0
webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFiter.java

@@ -0,0 +1,54 @@
+/*
+ *
+ *  * 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 org.springframework.security.web.server.authentication.logout;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.web.server.WebFilterExchange;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class LogoutWebFiter implements WebFilter {
+	private AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken("key", "anonymous",
+		AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
+	private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler();
+
+	private ServerWebExchangeMatcher requiresLogout = ServerWebExchangeMatchers
+		.pathMatchers("/logout");
+
+	@Override
+	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
+		return this.requiresLogout.matches(exchange)
+			.filter( result -> result.isMatch())
+			.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
+			.flatMap( result -> exchange.getPrincipal().cast(Authentication.class))
+			.defaultIfEmpty(this.anonymousAuthenticationToken)
+			.flatMap( authentication -> this.logoutHandler.logout(new WebFilterExchange(exchange, chain), authentication));
+	}
+}

+ 48 - 0
webflux/src/main/java/org/springframework/security/web/server/authentication/logout/SecurityContextRepositoryLogoutHandler.java

@@ -0,0 +1,48 @@
+/*
+ *
+ *  * 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 org.springframework.security.web.server.authentication.logout;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.DefaultRedirectStrategy;
+import org.springframework.security.web.server.RedirectStrategy;
+import org.springframework.security.web.server.context.SecurityContextRepository;
+import org.springframework.security.web.server.WebFilterExchange;
+import org.springframework.security.web.server.context.WebSessionSecurityContextRepository;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class SecurityContextRepositoryLogoutHandler implements LogoutHandler {
+	private SecurityContextRepository repository = new WebSessionSecurityContextRepository();
+
+	private URI logoutSuccessUrl = URI.create("/login?logout");
+
+	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+	@Override
+	public Mono<Void> logout(WebFilterExchange exchange,
+		Authentication authentication) {
+		return this.repository.save(exchange.getExchange(), null)
+			.then(this.redirectStrategy.sendRedirect(exchange.getExchange(), this.logoutSuccessUrl));
+	}
+}

+ 7 - 1
webflux/src/main/java/org/springframework/security/web/server/context/WebSessionSecurityContextRepository.java

@@ -32,7 +32,13 @@ public class WebSessionSecurityContextRepository implements SecurityContextRepos
 
 	public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
 		return exchange.getSession()
-			.doOnNext(session -> session.getAttributes().put(SESSION_ATTR, context))
+			.doOnNext(session -> {
+				if(context == null) {
+					session.getAttributes().remove(SESSION_ATTR);
+				} else {
+					session.getAttributes().put(SESSION_ATTR, context);
+				}
+			})
 			.then();
 	}
 

+ 12 - 3
webflux/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java

@@ -27,6 +27,7 @@ import org.springframework.http.MediaType;
 import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+import org.springframework.util.MultiValueMap;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilterChain;
@@ -50,18 +51,21 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
 	}
 
 	private Mono<Void> render(ServerWebExchange exchange) {
-		boolean isError = exchange.getRequest().getQueryParams().containsKey("error");
+		MultiValueMap<String, String> queryParams = exchange.getRequest()
+			.getQueryParams();
+		boolean isError = queryParams.containsKey("error");
+		boolean isLogoutSuccess = queryParams.containsKey("logout");
 		ServerHttpResponse result = exchange.getResponse();
 		result.setStatusCode(HttpStatus.FOUND);
 		result.getHeaders().setContentType(MediaType.TEXT_HTML);
-		byte[] bytes = createPage(isError);
+		byte[] bytes = createPage(isError, isLogoutSuccess);
 		DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
 		DataBuffer buffer = bufferFactory.wrap(bytes);
 		return result.writeWith(Mono.just(buffer))
 			.doOnError( error -> DataBufferUtils.release(buffer));
 	}
 
-	private static byte[] createPage(boolean isError) {
+	private static byte[] createPage(boolean isError, boolean isLogoutSuccess) {
 		String page =  "<!DOCTYPE html>\n"
 			+ "<html lang=\"en\">\n"
 			+ "  <head>\n"
@@ -78,6 +82,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
 			+ "      <form class=\"form-signin\" method=\"post\" action=\"/login\">\n"
 			+ "        <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
 			+ createError(isError)
+			+ createLogoutSuccess(isLogoutSuccess)
 			+ "        <p>\n"
 			+ "          <label for=\"username\" class=\"sr-only\">Username</label>\n"
 			+ "          <input type=\"text\" id=\"username\" name=\"username\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
@@ -98,4 +103,8 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
 	private static String createError(boolean isError) {
 		return isError ? "<div class=\"alert alert-danger\" role=\"alert\">Invalid credentials</div>" : "";
 	}
+
+	private static String createLogoutSuccess(boolean isLogoutSuccess) {
+		return isLogoutSuccess ? "<div class=\"alert alert-success\" role=\"alert\">You have been signed out</div>" : "";
+	}
 }