diff --git a/api/src/main/java/run/halo/app/security/LoginHandlerEnhancer.java b/api/src/main/java/run/halo/app/security/LoginHandlerEnhancer.java new file mode 100644 index 000000000..33c9a0788 --- /dev/null +++ b/api/src/main/java/run/halo/app/security/LoginHandlerEnhancer.java @@ -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; + +/** + *

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.

+ * + * @author guqing + * @since 2.17.0 + */ +public interface LoginHandlerEnhancer { + + /** + * Invoked when login success. + * + * @param exchange The exchange. + * @param successfulAuthentication The successful authentication. + */ + Mono onLoginSuccess(ServerWebExchange exchange, Authentication successfulAuthentication); + + /** + * Invoked when login fails. + * + * @param exchange The exchange. + * @param exception the reason authentication failed + */ + Mono onLoginFailure(ServerWebExchange exchange, AuthenticationException exception); +} diff --git a/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java index 3f929ef21..b195aa4ec 100644 --- a/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java @@ -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(); diff --git a/application/src/main/java/run/halo/app/security/LoginHandlerEnhancerImpl.java b/application/src/main/java/run/halo/app/security/LoginHandlerEnhancerImpl.java new file mode 100644 index 000000000..bb72c7ca0 --- /dev/null +++ b/application/src/main/java/run/halo/app/security/LoginHandlerEnhancerImpl.java @@ -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 onLoginSuccess(ServerWebExchange exchange, + Authentication successfulAuthentication) { + return rememberMeServices.loginSuccess(exchange, successfulAuthentication) + .then(deviceService.loginSuccess(exchange, successfulAuthentication)); + } + + @Override + public Mono onLoginFailure(ServerWebExchange exchange, + AuthenticationException exception) { + return rememberMeServices.loginFail(exchange); + } +} diff --git a/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java b/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java index edc8db0b6..8fff49645 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java +++ b/application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java @@ -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); diff --git a/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java index f36a7097b..33ce5b04d 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java +++ b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java @@ -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 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( diff --git a/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthSecurityConfigurer.java b/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthSecurityConfigurer.java index b53d289f9..d8dd6a770 100644 --- a/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthSecurityConfigurer.java +++ b/application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthSecurityConfigurer.java @@ -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); } } diff --git a/application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationFilter.java b/application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationFilter.java index 726fdcbed..b2140007d 100644 --- a/application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationFilter.java +++ b/application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationFilter.java @@ -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); }