Add support for redirection on logout (#7418)

#### 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 <http://localhost:8090/logout?redirect_uri=/archives> 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
登出页面支持自定义重定向
```
pull/7423/head
John Niang 2025-05-09 15:15:49 +08:00 committed by GitHub
parent 8a68a59ea5
commit c95d7b141b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 92 additions and 41 deletions

View File

@ -10,16 +10,22 @@ import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext; 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.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler; 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.ServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; 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.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
@ -34,9 +40,13 @@ import run.halo.app.theme.router.ModelConst;
@RequiredArgsConstructor @RequiredArgsConstructor
@Order(0) @Order(0)
public class LogoutSecurityConfigurer implements SecurityConfigurer { public class LogoutSecurityConfigurer implements SecurityConfigurer {
private final RememberMeServices rememberMeServices; private final RememberMeServices rememberMeServices;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final ServerRequestCache serverRequestCache = new HaloServerRequestCache();
@Override @Override
public void configure(ServerHttpSecurity http) { public void configure(ServerHttpSecurity http) {
var serverLogoutHandlers = getLogoutHandlers(); var serverLogoutHandlers = getLogoutHandlers();
@ -45,22 +55,6 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer {
); );
} }
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];
} else {
this.logoutHandler = new DelegatingServerLogoutHandler(logoutHandler);
}
}
@Bean @Bean
RouterFunction<ServerResponse> logoutPage( RouterFunction<ServerResponse> logoutPage(
UserService userService, UserService userService,
@ -74,6 +68,7 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer {
.flatMap(userService::getUser); .flatMap(userService::getUser);
var exchange = request.exchange(); var exchange = request.exchange();
var contextPath = exchange.getRequest().getPath().contextPath().value(); var contextPath = exchange.getRequest().getPath().contextPath().value();
return ServerResponse.ok().render("logout", Map.of( return ServerResponse.ok().render("logout", Map.of(
"globalInfo", globalInfoService.getGlobalInfo(), "globalInfo", globalInfoService.getGlobalInfo(),
"action", contextPath + "/logout", "action", contextPath + "/logout",
@ -84,9 +79,30 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer {
request.exchange().getAttributes().put(ModelConst.NO_CACHE, true); request.exchange().getAttributes().put(ModelConst.NO_CACHE, true);
return request; return request;
}) })
.filter((request, next) ->
// Save request before handling the logout
serverRequestCache.saveRequest(request.exchange()).then(next.handle(request))
)
.build(); .build();
} }
private class LogoutSuccessHandler implements ServerLogoutSuccessHandler {
private final ServerLogoutSuccessHandler defaultHandler;
private final ServerLogoutHandler logoutHandler;
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(logoutHandlers);
}
}
@Override @Override
public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, public Mono<Void> onLogoutSuccess(WebFilterExchange exchange,
Authentication authentication) { Authentication authentication) {
@ -94,13 +110,14 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer {
.then(rememberMeServices.loginFail(exchange.getExchange())) .then(rememberMeServices.loginFail(exchange.getExchange()))
.then(ignoringMediaTypeAll(MediaType.APPLICATION_JSON) .then(ignoringMediaTypeAll(MediaType.APPLICATION_JSON)
.matches(exchange.getExchange()) .matches(exchange.getExchange())
.flatMap(matchResult -> { .filter(ServerWebExchangeMatcher.MatchResult::isMatch)
if (matchResult.isMatch()) { .switchIfEmpty(Mono.defer(() ->
defaultHandler.onLogoutSuccess(exchange, authentication).then(Mono.empty())
))
.flatMap(match -> {
var response = exchange.getExchange().getResponse(); var response = exchange.getExchange().getResponse();
response.setStatusCode(HttpStatus.NO_CONTENT); response.setStatusCode(HttpStatus.NO_CONTENT);
return response.setComplete(); return response.setComplete();
}
return defaultHandler.onLogoutSuccess(exchange, authentication);
}) })
); );
} }
@ -110,4 +127,38 @@ public class LogoutSecurityConfigurer implements SecurityConfigurer {
return applicationContext.getBeansOfType(ServerLogoutHandler.class).values() return applicationContext.getBeansOfType(ServerLogoutHandler.class).values()
.toArray(new ServerLogoutHandler[0]); .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<Void> onLogoutSuccess(
WebFilterExchange exchange, Authentication authentication
) {
return this.requestCache.getRedirectUri(exchange.getExchange())
.defaultIfEmpty(this.location)
.flatMap(location ->
this.redirectStrategy.sendRedirect(exchange.getExchange(), location)
);
}
}
} }