From c95d7b141ba44f8800e5c10f8d47b5516de5b739 Mon Sep 17 00:00:00 2001 From: John Niang Date: Fri, 9 May 2025 15:15:49 +0800 Subject: [PATCH] Add support for redirection on logout (#7418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR adds support for redirection on logout. We can request with GET method, then click the logout to see the redirection. #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/7401 #### Does this PR introduce a user-facing change? ```release-note 登出页面支持自定义重定向 ``` --- .../security/LogoutSecurityConfigurer.java | 133 ++++++++++++------ 1 file changed, 92 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java b/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java index b8254dead..8917754a3 100644 --- a/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java +++ b/application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java @@ -10,16 +10,22 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.web.server.DefaultServerRedirectStrategy; +import org.springframework.security.web.server.ServerRedirectStrategy; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler; -import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import org.springframework.security.web.server.savedrequest.ServerRequestCache; +import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.stereotype.Component; +import org.springframework.util.Assert; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; @@ -34,9 +40,13 @@ import run.halo.app.theme.router.ModelConst; @RequiredArgsConstructor @Order(0) public class LogoutSecurityConfigurer implements SecurityConfigurer { + private final RememberMeServices rememberMeServices; + private final ApplicationContext applicationContext; + private final ServerRequestCache serverRequestCache = new HaloServerRequestCache(); + @Override public void configure(ServerHttpSecurity http) { var serverLogoutHandlers = getLogoutHandlers(); @@ -45,48 +55,54 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer { ); } + @Bean + RouterFunction logoutPage( + UserService userService, + GlobalInfoService globalInfoService + ) { + return RouterFunctions.route() + .GET("/logout", request -> { + var user = ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Authentication::getName) + .flatMap(userService::getUser); + var exchange = request.exchange(); + var contextPath = exchange.getRequest().getPath().contextPath().value(); + + return ServerResponse.ok().render("logout", Map.of( + "globalInfo", globalInfoService.getGlobalInfo(), + "action", contextPath + "/logout", + "user", user + )); + }) + .before(request -> { + request.exchange().getAttributes().put(ModelConst.NO_CACHE, true); + return request; + }) + .filter((request, next) -> + // Save request before handling the logout + serverRequestCache.saveRequest(request.exchange()).then(next.handle(request)) + ) + .build(); + } + + private class LogoutSuccessHandler implements ServerLogoutSuccessHandler { private final ServerLogoutSuccessHandler defaultHandler; private final ServerLogoutHandler logoutHandler; - public LogoutSuccessHandler(ServerLogoutHandler... logoutHandler) { - var defaultHandler = new RedirectServerLogoutSuccessHandler(); - defaultHandler.setLogoutSuccessUrl(URI.create("/login?logout")); - this.defaultHandler = defaultHandler; - if (logoutHandler.length == 1) { - this.logoutHandler = logoutHandler[0]; + public LogoutSuccessHandler(ServerLogoutHandler... logoutHandlers) { + var redirectHandler = new RequestCacheRedirectLogoutSuccessHandler(); + redirectHandler.setRequestCache(serverRequestCache); + this.defaultHandler = redirectHandler; + if (logoutHandlers.length == 1) { + this.logoutHandler = logoutHandlers[0]; } else { - this.logoutHandler = new DelegatingServerLogoutHandler(logoutHandler); + this.logoutHandler = new DelegatingServerLogoutHandler(logoutHandlers); } } - @Bean - RouterFunction logoutPage( - UserService userService, - GlobalInfoService globalInfoService - ) { - return RouterFunctions.route() - .GET("/logout", request -> { - var user = ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .map(Authentication::getName) - .flatMap(userService::getUser); - var exchange = request.exchange(); - var contextPath = exchange.getRequest().getPath().contextPath().value(); - return ServerResponse.ok().render("logout", Map.of( - "globalInfo", globalInfoService.getGlobalInfo(), - "action", contextPath + "/logout", - "user", user - )); - }) - .before(request -> { - request.exchange().getAttributes().put(ModelConst.NO_CACHE, true); - return request; - }) - .build(); - } - @Override public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) { @@ -94,13 +110,14 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer { .then(rememberMeServices.loginFail(exchange.getExchange())) .then(ignoringMediaTypeAll(MediaType.APPLICATION_JSON) .matches(exchange.getExchange()) - .flatMap(matchResult -> { - if (matchResult.isMatch()) { - var response = exchange.getExchange().getResponse(); - response.setStatusCode(HttpStatus.NO_CONTENT); - return response.setComplete(); - } - return defaultHandler.onLogoutSuccess(exchange, authentication); + .filter(ServerWebExchangeMatcher.MatchResult::isMatch) + .switchIfEmpty(Mono.defer(() -> + defaultHandler.onLogoutSuccess(exchange, authentication).then(Mono.empty()) + )) + .flatMap(match -> { + var response = exchange.getExchange().getResponse(); + response.setStatusCode(HttpStatus.NO_CONTENT); + return response.setComplete(); }) ); } @@ -110,4 +127,38 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer { return applicationContext.getBeansOfType(ServerLogoutHandler.class).values() .toArray(new ServerLogoutHandler[0]); } + + private static class RequestCacheRedirectLogoutSuccessHandler + implements ServerLogoutSuccessHandler { + + private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); + + private URI location = URI.create("/login?logout"); + + private ServerRequestCache requestCache = new WebSessionServerRequestCache(); + + public RequestCacheRedirectLogoutSuccessHandler() { + } + + public RequestCacheRedirectLogoutSuccessHandler(String location) { + this.location = URI.create(location); + } + + public void setRequestCache(@NonNull ServerRequestCache requestCache) { + Assert.notNull(requestCache, "requestCache cannot be null"); + this.requestCache = requestCache; + } + + @Override + public Mono onLogoutSuccess( + WebFilterExchange exchange, Authentication authentication + ) { + return this.requestCache.getRedirectUri(exchange.getExchange()) + .defaultIfEmpty(this.location) + .flatMap(location -> + this.redirectStrategy.sendRedirect(exchange.getExchange(), location) + ); + } + + } }