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.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<ServerResponse> 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<ServerResponse> 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<Void> 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<Void> onLogoutSuccess(
WebFilterExchange exchange, Authentication authentication
) {
return this.requestCache.getRedirectUri(exchange.getExchange())
.defaultIfEmpty(this.location)
.flatMap(location ->
this.redirectStrategy.sendRedirect(exchange.getExchange(), location)
);
}
}
}