feat: add LoginHandlerEnhancer for enhanced login processing (#6176)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.17.x

#### What this PR does / why we need it:
新增 LoginHandlerEnhancer 用于 Halo 扩展登录成功或失败后的处理逻辑如 RememberMe 和设备管理等

#### Does this PR introduce a user-facing change?
```release-note
None
```
pull/6211/head
guqing 2024-07-01 14:49:16 +08:00 committed by GitHub
parent d92bb4398e
commit 967eaa21e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 97 additions and 38 deletions

View File

@ -0,0 +1,34 @@
package run.halo.app.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* <p>Halo uses this interface to enhance the processing of login success, such as device management
* and remember me, etc. The login method of the plugin extension needs to call this interface in
* the processing method of login success to ensure the normal operation of some enhanced
* functions.</p>
*
* @author guqing
* @since 2.17.0
*/
public interface LoginHandlerEnhancer {
/**
* Invoked when login success.
*
* @param exchange The exchange.
* @param successfulAuthentication The successful authentication.
*/
Mono<Void> onLoginSuccess(ServerWebExchange exchange, Authentication successfulAuthentication);
/**
* Invoked when login fails.
*
* @param exchange The exchange.
* @param exception the reason authentication failed
*/
Mono<Void> onLoginFailure(ServerWebExchange exchange, AuthenticationException exception);
}

View File

@ -14,6 +14,7 @@ import run.halo.app.infra.ExternalLinkProcessor;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.notification.NotificationCenter;
import run.halo.app.notification.NotificationReasonEmitter;
import run.halo.app.security.LoginHandlerEnhancer;
/**
* Utility for creating shared application context.
@ -59,6 +60,8 @@ public enum SharedApplicationContextFactory {
rootContext.getBean(PostContentService.class));
beanFactory.registerSingleton("cacheManager",
rootContext.getBean(CacheManager.class));
beanFactory.registerSingleton("loginHandlerEnhancer",
rootContext.getBean(LoginHandlerEnhancer.class));
// TODO add more shared instance here
sharedContext.refresh();

View File

@ -0,0 +1,39 @@
package run.halo.app.security;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.security.authentication.rememberme.RememberMeServices;
import run.halo.app.security.device.DeviceService;
/**
* A default implementation for {@link LoginHandlerEnhancer} to handle device management and
* remember me.
*
* @author guqing
* @since 2.17.0
*/
@Component
@RequiredArgsConstructor
public class LoginHandlerEnhancerImpl implements LoginHandlerEnhancer {
private final RememberMeServices rememberMeServices;
private final DeviceService deviceService;
@Override
public Mono<Void> onLoginSuccess(ServerWebExchange exchange,
Authentication successfulAuthentication) {
return rememberMeServices.loginSuccess(exchange, successfulAuthentication)
.then(deviceService.loginSuccess(exchange, successfulAuthentication));
}
@Override
public Mono<Void> onLoginFailure(ServerWebExchange exchange,
AuthenticationException exception) {
return rememberMeServices.loginFail(exchange);
}
}

View File

@ -18,10 +18,9 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.security.LoginHandlerEnhancer;
import run.halo.app.security.authentication.CryptoService;
import run.halo.app.security.authentication.SecurityConfigurer;
import run.halo.app.security.authentication.rememberme.RememberMeServices;
import run.halo.app.security.device.DeviceService;
@Component
public class LoginSecurityConfigurer implements SecurityConfigurer {
@ -43,8 +42,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
private final MessageSource messageSource;
private final RateLimiterRegistry rateLimiterRegistry;
private final RememberMeServices rememberMeServices;
private final DeviceService deviceService;
private final LoginHandlerEnhancer loginHandlerEnhancer;
public LoginSecurityConfigurer(ObservationRegistry observationRegistry,
ReactiveUserDetailsService userDetailsService,
@ -52,8 +50,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
ServerSecurityContextRepository securityContextRepository, CryptoService cryptoService,
ExtensionGetter extensionGetter, ServerResponse.Context context,
MessageSource messageSource, RateLimiterRegistry rateLimiterRegistry,
RememberMeServices rememberMeServices,
DeviceService deviceService) {
LoginHandlerEnhancer loginHandlerEnhancer) {
this.observationRegistry = observationRegistry;
this.userDetailsService = userDetailsService;
this.passwordService = passwordService;
@ -64,8 +61,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
this.context = context;
this.messageSource = messageSource;
this.rateLimiterRegistry = rateLimiterRegistry;
this.rememberMeServices = rememberMeServices;
this.deviceService = deviceService;
this.loginHandlerEnhancer = loginHandlerEnhancer;
}
@Override
@ -73,7 +69,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
var filter = new AuthenticationWebFilter(authenticationManager());
var requiresMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login");
var handler =
new UsernamePasswordHandler(context, messageSource, rememberMeServices, deviceService);
new UsernamePasswordHandler(context, messageSource, loginHandlerEnhancer);
var authConverter = new LoginAuthenticationConverter(cryptoService, rateLimiterRegistry);
filter.setRequiresAuthenticationMatcher(requiresMatcher);
filter.setAuthenticationFailureHandler(handler);

View File

@ -20,9 +20,8 @@ import org.springframework.web.ErrorResponse;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.security.authentication.rememberme.RememberMeServices;
import run.halo.app.security.LoginHandlerEnhancer;
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
import run.halo.app.security.device.DeviceService;
@Slf4j
public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandler,
@ -32,9 +31,7 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
private final MessageSource messageSource;
private final RememberMeServices rememberMeServices;
private final DeviceService deviceService;
private final LoginHandlerEnhancer loginHandlerEnhancer;
private final ServerAuthenticationFailureHandler defaultFailureHandler =
new RedirectServerAuthenticationFailureHandler("/console?error#/login");
@ -43,18 +40,17 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
new RedirectServerAuthenticationSuccessHandler("/console/");
public UsernamePasswordHandler(ServerResponse.Context context, MessageSource messageSource,
RememberMeServices rememberMeServices, DeviceService deviceService) {
LoginHandlerEnhancer loginHandlerEnhancer) {
this.context = context;
this.messageSource = messageSource;
this.rememberMeServices = rememberMeServices;
this.deviceService = deviceService;
this.loginHandlerEnhancer = loginHandlerEnhancer;
}
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange,
AuthenticationException exception) {
var exchange = webFilterExchange.getExchange();
return rememberMeServices.loginFail(exchange)
return loginHandlerEnhancer.onLoginFailure(exchange, exception)
.then(ignoringMediaTypeAll(APPLICATION_JSON)
.matches(exchange)
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
@ -71,8 +67,8 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
Authentication authentication) {
if (authentication instanceof TwoFactorAuthentication) {
// continue filtering for authorization
return rememberMeServices.loginSuccess(webFilterExchange.getExchange(), authentication)
.then(deviceService.loginSuccess(webFilterExchange.getExchange(), authentication))
return loginHandlerEnhancer.onLoginSuccess(webFilterExchange.getExchange(),
authentication)
.then(webFilterExchange.getChain().filter(webFilterExchange.getExchange()));
}
@ -89,8 +85,7 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
};
var exchange = webFilterExchange.getExchange();
return rememberMeServices.loginSuccess(exchange, authentication)
.then(deviceService.loginSuccess(exchange, authentication))
return loginHandlerEnhancer.onLoginSuccess(webFilterExchange.getExchange(), authentication)
.then(xhrMatcher.matches(exchange)
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.switchIfEmpty(Mono.defer(

View File

@ -6,11 +6,10 @@ import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.security.LoginHandlerEnhancer;
import run.halo.app.security.authentication.SecurityConfigurer;
import run.halo.app.security.authentication.rememberme.RememberMeServices;
import run.halo.app.security.authentication.twofactor.totp.TotpAuthService;
import run.halo.app.security.authentication.twofactor.totp.TotpAuthenticationFilter;
import run.halo.app.security.device.DeviceService;
@Component
public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
@ -23,30 +22,26 @@ public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
private final MessageSource messageSource;
private final RememberMeServices rememberMeServices;
private final DeviceService deviceService;
private final LoginHandlerEnhancer loginHandlerEnhancer;
public TwoFactorAuthSecurityConfigurer(
ServerSecurityContextRepository securityContextRepository,
TotpAuthService totpAuthService,
ServerResponse.Context context,
MessageSource messageSource,
RememberMeServices rememberMeServices,
DeviceService deviceService
LoginHandlerEnhancer loginHandlerEnhancer
) {
this.securityContextRepository = securityContextRepository;
this.totpAuthService = totpAuthService;
this.context = context;
this.messageSource = messageSource;
this.rememberMeServices = rememberMeServices;
this.deviceService = deviceService;
this.loginHandlerEnhancer = loginHandlerEnhancer;
}
@Override
public void configure(ServerHttpSecurity http) {
var filter = new TotpAuthenticationFilter(securityContextRepository, totpAuthService,
context, messageSource, rememberMeServices, deviceService);
context, messageSource, loginHandlerEnhancer);
http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHENTICATION);
}
}

View File

@ -19,10 +19,9 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.security.HaloUserDetails;
import run.halo.app.security.LoginHandlerEnhancer;
import run.halo.app.security.authentication.login.UsernamePasswordHandler;
import run.halo.app.security.authentication.rememberme.RememberMeServices;
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
import run.halo.app.security.device.DeviceService;
@Slf4j
public class TotpAuthenticationFilter extends AuthenticationWebFilter {
@ -32,8 +31,7 @@ public class TotpAuthenticationFilter extends AuthenticationWebFilter {
TotpAuthService totpAuthService,
ServerResponse.Context context,
MessageSource messageSource,
RememberMeServices rememberMeServices,
DeviceService deviceService
LoginHandlerEnhancer loginHandlerEnhancer
) {
super(new TwoFactorAuthManager(totpAuthService));
@ -41,8 +39,7 @@ public class TotpAuthenticationFilter extends AuthenticationWebFilter {
setRequiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login/2fa/totp"));
setServerAuthenticationConverter(new TotpCodeAuthenticationConverter());
var handler =
new UsernamePasswordHandler(context, messageSource, rememberMeServices, deviceService);
var handler = new UsernamePasswordHandler(context, messageSource, loginHandlerEnhancer);
setAuthenticationSuccessHandler(handler);
setAuthenticationFailureHandler(handler);
}