parent
426e24c18e
commit
e14af37775
|
@ -24,6 +24,7 @@ import java.util.List;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.web.server.DelegatingAuthenticationEntryPoint;
|
import org.springframework.security.web.server.DelegatingAuthenticationEntryPoint;
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationFailureHandler;
|
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.server.util.matcher.MediaTypeServerWebExchangeMatcher;
|
||||||
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -175,6 +176,7 @@ public class HttpSecurity {
|
||||||
filters.add(new LoginPageGeneratingWebFilter());
|
filters.add(new LoginPageGeneratingWebFilter());
|
||||||
}
|
}
|
||||||
filters.add(this.formLogin.build());
|
filters.add(this.formLogin.build());
|
||||||
|
filters.add(new LogoutWebFiter());
|
||||||
}
|
}
|
||||||
filters.add(new AuthenticationReactorContextFilter());
|
filters.add(new AuthenticationReactorContextFilter());
|
||||||
if(this.authorizeExchangeBuilder != null) {
|
if(this.authorizeExchangeBuilder != null) {
|
||||||
|
|
|
@ -67,7 +67,14 @@ public class FormLoginTests {
|
||||||
.webTestClientSetup(webTestClient)
|
.webTestClientSetup(webTestClient)
|
||||||
.build();
|
.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()
|
HomePage homePage = loginPage.loginForm()
|
||||||
.username("user")
|
.username("user")
|
||||||
|
@ -75,6 +82,12 @@ public class FormLoginTests {
|
||||||
.submit(HomePage.class);
|
.submit(HomePage.class);
|
||||||
|
|
||||||
homePage.assertAt();
|
homePage.assertAt();
|
||||||
|
|
||||||
|
driver.get("http://localhost/logout");
|
||||||
|
|
||||||
|
DefaultLoginPage.create(driver)
|
||||||
|
.assertAt()
|
||||||
|
.assertLogout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -161,6 +174,8 @@ public class FormLoginTests {
|
||||||
public static class DefaultLoginPage {
|
public static class DefaultLoginPage {
|
||||||
|
|
||||||
private WebDriver driver;
|
private WebDriver driver;
|
||||||
|
@FindBy(css = "div[role=alert]")
|
||||||
|
private WebElement alert;
|
||||||
|
|
||||||
private LoginForm loginForm;
|
private LoginForm loginForm;
|
||||||
|
|
||||||
|
@ -169,6 +184,25 @@ public class FormLoginTests {
|
||||||
this.loginForm = PageFactory.initElements(webDriver, LoginForm.class);
|
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() {
|
public LoginForm loginForm() {
|
||||||
return this.loginForm;
|
return this.loginForm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,13 @@ public class WebSessionSecurityContextRepository implements SecurityContextRepos
|
||||||
|
|
||||||
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
|
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
|
||||||
return exchange.getSession()
|
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();
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
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.ServerWebExchange;
|
||||||
import org.springframework.web.server.WebFilter;
|
import org.springframework.web.server.WebFilter;
|
||||||
import org.springframework.web.server.WebFilterChain;
|
import org.springframework.web.server.WebFilterChain;
|
||||||
|
@ -50,18 +51,21 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> render(ServerWebExchange exchange) {
|
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();
|
ServerHttpResponse result = exchange.getResponse();
|
||||||
result.setStatusCode(HttpStatus.FOUND);
|
result.setStatusCode(HttpStatus.FOUND);
|
||||||
result.getHeaders().setContentType(MediaType.TEXT_HTML);
|
result.getHeaders().setContentType(MediaType.TEXT_HTML);
|
||||||
byte[] bytes = createPage(isError);
|
byte[] bytes = createPage(isError, isLogoutSuccess);
|
||||||
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
|
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
|
||||||
DataBuffer buffer = bufferFactory.wrap(bytes);
|
DataBuffer buffer = bufferFactory.wrap(bytes);
|
||||||
return result.writeWith(Mono.just(buffer))
|
return result.writeWith(Mono.just(buffer))
|
||||||
.doOnError( error -> DataBufferUtils.release(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"
|
String page = "<!DOCTYPE html>\n"
|
||||||
+ "<html lang=\"en\">\n"
|
+ "<html lang=\"en\">\n"
|
||||||
+ " <head>\n"
|
+ " <head>\n"
|
||||||
|
@ -78,6 +82,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter {
|
||||||
+ " <form class=\"form-signin\" method=\"post\" action=\"/login\">\n"
|
+ " <form class=\"form-signin\" method=\"post\" action=\"/login\">\n"
|
||||||
+ " <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
|
+ " <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
|
||||||
+ createError(isError)
|
+ createError(isError)
|
||||||
|
+ createLogoutSuccess(isLogoutSuccess)
|
||||||
+ " <p>\n"
|
+ " <p>\n"
|
||||||
+ " <label for=\"username\" class=\"sr-only\">Username</label>\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"
|
+ " <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) {
|
private static String createError(boolean isError) {
|
||||||
return isError ? "<div class=\"alert alert-danger\" role=\"alert\">Invalid credentials</div>" : "";
|
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>" : "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue