Add LogoutWebFilter

Fixes gh-4539
This commit is contained in:
Rob Winch 2017-09-13 16:43:04 -05:00
parent 426e24c18e
commit e14af37775
7 changed files with 189 additions and 5 deletions

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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();
}

View File

@ -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>" : "";
}
}