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,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 class LogoutSuccessHandler implements ServerLogoutSuccessHandler {
private final ServerLogoutSuccessHandler defaultHandler; private final ServerLogoutSuccessHandler defaultHandler;
private final ServerLogoutHandler logoutHandler; private final ServerLogoutHandler logoutHandler;
public LogoutSuccessHandler(ServerLogoutHandler... logoutHandler) { public LogoutSuccessHandler(ServerLogoutHandler... logoutHandlers) {
var defaultHandler = new RedirectServerLogoutSuccessHandler(); var redirectHandler = new RequestCacheRedirectLogoutSuccessHandler();
defaultHandler.setLogoutSuccessUrl(URI.create("/login?logout")); redirectHandler.setRequestCache(serverRequestCache);
this.defaultHandler = defaultHandler; this.defaultHandler = redirectHandler;
if (logoutHandler.length == 1) { if (logoutHandlers.length == 1) {
this.logoutHandler = logoutHandler[0]; this.logoutHandler = logoutHandlers[0];
} else { } 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 @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(() ->
var response = exchange.getExchange().getResponse(); defaultHandler.onLogoutSuccess(exchange, authentication).then(Mono.empty())
response.setStatusCode(HttpStatus.NO_CONTENT); ))
return response.setComplete(); .flatMap(match -> {
} var response = exchange.getExchange().getResponse();
return defaultHandler.onLogoutSuccess(exchange, authentication); response.setStatusCode(HttpStatus.NO_CONTENT);
return response.setComplete();
}) })
); );
} }
@ -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)
);
}
}
} }