mirror of https://github.com/halo-dev/halo
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
parent
d92bb4398e
commit
967eaa21e1
|
@ -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);
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import run.halo.app.infra.ExternalLinkProcessor;
|
||||||
import run.halo.app.infra.ExternalUrlSupplier;
|
import run.halo.app.infra.ExternalUrlSupplier;
|
||||||
import run.halo.app.notification.NotificationCenter;
|
import run.halo.app.notification.NotificationCenter;
|
||||||
import run.halo.app.notification.NotificationReasonEmitter;
|
import run.halo.app.notification.NotificationReasonEmitter;
|
||||||
|
import run.halo.app.security.LoginHandlerEnhancer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for creating shared application context.
|
* Utility for creating shared application context.
|
||||||
|
@ -59,6 +60,8 @@ public enum SharedApplicationContextFactory {
|
||||||
rootContext.getBean(PostContentService.class));
|
rootContext.getBean(PostContentService.class));
|
||||||
beanFactory.registerSingleton("cacheManager",
|
beanFactory.registerSingleton("cacheManager",
|
||||||
rootContext.getBean(CacheManager.class));
|
rootContext.getBean(CacheManager.class));
|
||||||
|
beanFactory.registerSingleton("loginHandlerEnhancer",
|
||||||
|
rootContext.getBean(LoginHandlerEnhancer.class));
|
||||||
// TODO add more shared instance here
|
// TODO add more shared instance here
|
||||||
|
|
||||||
sharedContext.refresh();
|
sharedContext.refresh();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,10 +18,9 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
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.CryptoService;
|
||||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||||
import run.halo.app.security.authentication.rememberme.RememberMeServices;
|
|
||||||
import run.halo.app.security.device.DeviceService;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class LoginSecurityConfigurer implements SecurityConfigurer {
|
public class LoginSecurityConfigurer implements SecurityConfigurer {
|
||||||
|
@ -43,8 +42,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
|
||||||
private final MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
private final RateLimiterRegistry rateLimiterRegistry;
|
private final RateLimiterRegistry rateLimiterRegistry;
|
||||||
|
|
||||||
private final RememberMeServices rememberMeServices;
|
private final LoginHandlerEnhancer loginHandlerEnhancer;
|
||||||
private final DeviceService deviceService;
|
|
||||||
|
|
||||||
public LoginSecurityConfigurer(ObservationRegistry observationRegistry,
|
public LoginSecurityConfigurer(ObservationRegistry observationRegistry,
|
||||||
ReactiveUserDetailsService userDetailsService,
|
ReactiveUserDetailsService userDetailsService,
|
||||||
|
@ -52,8 +50,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
|
||||||
ServerSecurityContextRepository securityContextRepository, CryptoService cryptoService,
|
ServerSecurityContextRepository securityContextRepository, CryptoService cryptoService,
|
||||||
ExtensionGetter extensionGetter, ServerResponse.Context context,
|
ExtensionGetter extensionGetter, ServerResponse.Context context,
|
||||||
MessageSource messageSource, RateLimiterRegistry rateLimiterRegistry,
|
MessageSource messageSource, RateLimiterRegistry rateLimiterRegistry,
|
||||||
RememberMeServices rememberMeServices,
|
LoginHandlerEnhancer loginHandlerEnhancer) {
|
||||||
DeviceService deviceService) {
|
|
||||||
this.observationRegistry = observationRegistry;
|
this.observationRegistry = observationRegistry;
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
this.passwordService = passwordService;
|
this.passwordService = passwordService;
|
||||||
|
@ -64,8 +61,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.rateLimiterRegistry = rateLimiterRegistry;
|
this.rateLimiterRegistry = rateLimiterRegistry;
|
||||||
this.rememberMeServices = rememberMeServices;
|
this.loginHandlerEnhancer = loginHandlerEnhancer;
|
||||||
this.deviceService = deviceService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -73,7 +69,7 @@ public class LoginSecurityConfigurer implements SecurityConfigurer {
|
||||||
var filter = new AuthenticationWebFilter(authenticationManager());
|
var filter = new AuthenticationWebFilter(authenticationManager());
|
||||||
var requiresMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login");
|
var requiresMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login");
|
||||||
var handler =
|
var handler =
|
||||||
new UsernamePasswordHandler(context, messageSource, rememberMeServices, deviceService);
|
new UsernamePasswordHandler(context, messageSource, loginHandlerEnhancer);
|
||||||
var authConverter = new LoginAuthenticationConverter(cryptoService, rateLimiterRegistry);
|
var authConverter = new LoginAuthenticationConverter(cryptoService, rateLimiterRegistry);
|
||||||
filter.setRequiresAuthenticationMatcher(requiresMatcher);
|
filter.setRequiresAuthenticationMatcher(requiresMatcher);
|
||||||
filter.setAuthenticationFailureHandler(handler);
|
filter.setAuthenticationFailureHandler(handler);
|
||||||
|
|
|
@ -20,9 +20,8 @@ import org.springframework.web.ErrorResponse;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
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.authentication.twofactor.TwoFactorAuthentication;
|
||||||
import run.halo.app.security.device.DeviceService;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandler,
|
public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandler,
|
||||||
|
@ -32,9 +31,7 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
|
||||||
|
|
||||||
private final MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
private final RememberMeServices rememberMeServices;
|
private final LoginHandlerEnhancer loginHandlerEnhancer;
|
||||||
|
|
||||||
private final DeviceService deviceService;
|
|
||||||
|
|
||||||
private final ServerAuthenticationFailureHandler defaultFailureHandler =
|
private final ServerAuthenticationFailureHandler defaultFailureHandler =
|
||||||
new RedirectServerAuthenticationFailureHandler("/console?error#/login");
|
new RedirectServerAuthenticationFailureHandler("/console?error#/login");
|
||||||
|
@ -43,18 +40,17 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
|
||||||
new RedirectServerAuthenticationSuccessHandler("/console/");
|
new RedirectServerAuthenticationSuccessHandler("/console/");
|
||||||
|
|
||||||
public UsernamePasswordHandler(ServerResponse.Context context, MessageSource messageSource,
|
public UsernamePasswordHandler(ServerResponse.Context context, MessageSource messageSource,
|
||||||
RememberMeServices rememberMeServices, DeviceService deviceService) {
|
LoginHandlerEnhancer loginHandlerEnhancer) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.rememberMeServices = rememberMeServices;
|
this.loginHandlerEnhancer = loginHandlerEnhancer;
|
||||||
this.deviceService = deviceService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange,
|
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange,
|
||||||
AuthenticationException exception) {
|
AuthenticationException exception) {
|
||||||
var exchange = webFilterExchange.getExchange();
|
var exchange = webFilterExchange.getExchange();
|
||||||
return rememberMeServices.loginFail(exchange)
|
return loginHandlerEnhancer.onLoginFailure(exchange, exception)
|
||||||
.then(ignoringMediaTypeAll(APPLICATION_JSON)
|
.then(ignoringMediaTypeAll(APPLICATION_JSON)
|
||||||
.matches(exchange)
|
.matches(exchange)
|
||||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||||
|
@ -71,8 +67,8 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
if (authentication instanceof TwoFactorAuthentication) {
|
if (authentication instanceof TwoFactorAuthentication) {
|
||||||
// continue filtering for authorization
|
// continue filtering for authorization
|
||||||
return rememberMeServices.loginSuccess(webFilterExchange.getExchange(), authentication)
|
return loginHandlerEnhancer.onLoginSuccess(webFilterExchange.getExchange(),
|
||||||
.then(deviceService.loginSuccess(webFilterExchange.getExchange(), authentication))
|
authentication)
|
||||||
.then(webFilterExchange.getChain().filter(webFilterExchange.getExchange()));
|
.then(webFilterExchange.getChain().filter(webFilterExchange.getExchange()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,8 +85,7 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
|
||||||
};
|
};
|
||||||
|
|
||||||
var exchange = webFilterExchange.getExchange();
|
var exchange = webFilterExchange.getExchange();
|
||||||
return rememberMeServices.loginSuccess(exchange, authentication)
|
return loginHandlerEnhancer.onLoginSuccess(webFilterExchange.getExchange(), authentication)
|
||||||
.then(deviceService.loginSuccess(exchange, authentication))
|
|
||||||
.then(xhrMatcher.matches(exchange)
|
.then(xhrMatcher.matches(exchange)
|
||||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||||
.switchIfEmpty(Mono.defer(
|
.switchIfEmpty(Mono.defer(
|
||||||
|
|
|
@ -6,11 +6,10 @@ import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
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.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.TotpAuthService;
|
||||||
import run.halo.app.security.authentication.twofactor.totp.TotpAuthenticationFilter;
|
import run.halo.app.security.authentication.twofactor.totp.TotpAuthenticationFilter;
|
||||||
import run.halo.app.security.device.DeviceService;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
|
public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
|
||||||
|
@ -23,30 +22,26 @@ public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
|
||||||
|
|
||||||
private final MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
private final RememberMeServices rememberMeServices;
|
private final LoginHandlerEnhancer loginHandlerEnhancer;
|
||||||
|
|
||||||
private final DeviceService deviceService;
|
|
||||||
|
|
||||||
public TwoFactorAuthSecurityConfigurer(
|
public TwoFactorAuthSecurityConfigurer(
|
||||||
ServerSecurityContextRepository securityContextRepository,
|
ServerSecurityContextRepository securityContextRepository,
|
||||||
TotpAuthService totpAuthService,
|
TotpAuthService totpAuthService,
|
||||||
ServerResponse.Context context,
|
ServerResponse.Context context,
|
||||||
MessageSource messageSource,
|
MessageSource messageSource,
|
||||||
RememberMeServices rememberMeServices,
|
LoginHandlerEnhancer loginHandlerEnhancer
|
||||||
DeviceService deviceService
|
|
||||||
) {
|
) {
|
||||||
this.securityContextRepository = securityContextRepository;
|
this.securityContextRepository = securityContextRepository;
|
||||||
this.totpAuthService = totpAuthService;
|
this.totpAuthService = totpAuthService;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.rememberMeServices = rememberMeServices;
|
this.loginHandlerEnhancer = loginHandlerEnhancer;
|
||||||
this.deviceService = deviceService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure(ServerHttpSecurity http) {
|
public void configure(ServerHttpSecurity http) {
|
||||||
var filter = new TotpAuthenticationFilter(securityContextRepository, totpAuthService,
|
var filter = new TotpAuthenticationFilter(securityContextRepository, totpAuthService,
|
||||||
context, messageSource, rememberMeServices, deviceService);
|
context, messageSource, loginHandlerEnhancer);
|
||||||
http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHENTICATION);
|
http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,9 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.security.HaloUserDetails;
|
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.login.UsernamePasswordHandler;
|
||||||
import run.halo.app.security.authentication.rememberme.RememberMeServices;
|
|
||||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||||
import run.halo.app.security.device.DeviceService;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TotpAuthenticationFilter extends AuthenticationWebFilter {
|
public class TotpAuthenticationFilter extends AuthenticationWebFilter {
|
||||||
|
@ -32,8 +31,7 @@ public class TotpAuthenticationFilter extends AuthenticationWebFilter {
|
||||||
TotpAuthService totpAuthService,
|
TotpAuthService totpAuthService,
|
||||||
ServerResponse.Context context,
|
ServerResponse.Context context,
|
||||||
MessageSource messageSource,
|
MessageSource messageSource,
|
||||||
RememberMeServices rememberMeServices,
|
LoginHandlerEnhancer loginHandlerEnhancer
|
||||||
DeviceService deviceService
|
|
||||||
) {
|
) {
|
||||||
super(new TwoFactorAuthManager(totpAuthService));
|
super(new TwoFactorAuthManager(totpAuthService));
|
||||||
|
|
||||||
|
@ -41,8 +39,7 @@ public class TotpAuthenticationFilter extends AuthenticationWebFilter {
|
||||||
setRequiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login/2fa/totp"));
|
setRequiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login/2fa/totp"));
|
||||||
setServerAuthenticationConverter(new TotpCodeAuthenticationConverter());
|
setServerAuthenticationConverter(new TotpCodeAuthenticationConverter());
|
||||||
|
|
||||||
var handler =
|
var handler = new UsernamePasswordHandler(context, messageSource, loginHandlerEnhancer);
|
||||||
new UsernamePasswordHandler(context, messageSource, rememberMeServices, deviceService);
|
|
||||||
setAuthenticationSuccessHandler(handler);
|
setAuthenticationSuccessHandler(handler);
|
||||||
setAuthenticationFailureHandler(handler);
|
setAuthenticationFailureHandler(handler);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue