From 9d01b627d0fee7671dcc9f07b751a8421bd6ea5a Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 7 Oct 2024 23:50:53 +0800 Subject: [PATCH] Customize authorization exchange separately (#6779) #### What type of PR is this? /kind cleanup /area core /milestone 2.20.x #### What this PR does / why we need it: This PR separates authorization exchange customization into security configurers. I also define the annotations `@Order` on every security configurer in order to customize authorization exchange in separated source file instead of modifying existing. #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../infra/config/WebServerSecurityConfig.java | 44 +------ .../AuthorizationExchangeConfigurers.java | 107 ++++++++++++++++++ .../NotAuthenticatedAuthorizationManager.java | 30 ----- 3 files changed, 108 insertions(+), 73 deletions(-) create mode 100644 application/src/main/java/run/halo/app/security/authorization/AuthorizationExchangeConfigurers.java delete mode 100644 application/src/main/java/run/halo/app/security/authorization/NotAuthenticatedAuthorizationManager.java diff --git a/application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java b/application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java index 4385ae974..6acb68905 100644 --- a/application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java +++ b/application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java @@ -3,7 +3,6 @@ package run.halo.app.infra.config; import static org.springframework.security.web.server.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder; import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; -import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.ObjectProvider; @@ -12,11 +11,8 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.core.Authentication; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -24,13 +20,9 @@ import org.springframework.security.web.server.context.ServerSecurityContextRepo import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; import org.springframework.security.web.server.savedrequest.ServerRequestCache; import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.session.MapSession; import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; -import reactor.core.publisher.Mono; import run.halo.app.core.user.service.RoleService; import run.halo.app.core.user.service.UserService; import run.halo.app.extension.ReactiveExtensionClient; @@ -44,8 +36,6 @@ import run.halo.app.security.authentication.impl.RsaKeyService; import run.halo.app.security.authentication.pat.PatAuthenticationManager; import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher; import run.halo.app.security.authorization.AuthorityUtils; -import run.halo.app.security.authorization.NotAuthenticatedAuthorizationManager; -import run.halo.app.security.authorization.RequestInfoAuthorizationManager; import run.halo.app.security.session.InMemoryReactiveIndexedSessionRepository; import run.halo.app.security.session.ReactiveIndexedSessionRepository; @@ -86,29 +76,6 @@ public class WebServerSecurityConfig { new NegatedServerWebExchangeMatcher(staticResourcesMatcher)); http.securityMatcher(securityMatcher) - .authorizeExchange(spec -> spec.pathMatchers( - "/api/**", - "/apis/**", - "/actuator/**" - ).access(new RequestInfoAuthorizationManager(roleService)) - .pathMatchers(HttpMethod.GET, "/login", "/signup") - .access(new NotAuthenticatedAuthorizationManager()) - .pathMatchers( - "/login/**", - "/challenges/**", - "/password-reset/**", - "/signup", - "/logout" - ).permitAll() - .pathMatchers("/console/**", "/uc/**").authenticated() - .matchers(createHtmlMatcher()).access((authentication, context) -> - // we only need to check the authentication is authenticated - // because we treat anonymous user as authenticated - authentication.map(Authentication::isAuthenticated) - .map(AuthorizationDecision::new) - .switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false))) - ) - .anyExchange().permitAll()) .anonymous(spec -> { spec.authorities(AuthorityUtils.ROLE_PREFIX + AnonymousUserConst.Role); spec.principal(AnonymousUserConst.PRINCIPAL); @@ -190,14 +157,5 @@ public class WebServerSecurityConfig { return new RsaKeyService(haloProperties.getWorkDir().resolve("keys")); } - private static ServerWebExchangeMatcher createHtmlMatcher() { - ServerWebExchangeMatcher get = - ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**"); - ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers("/favicon.*")); - MediaTypeServerWebExchangeMatcher html = - new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML); - html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); - return new AndServerWebExchangeMatcher(get, notFavicon, html); - } + } diff --git a/application/src/main/java/run/halo/app/security/authorization/AuthorizationExchangeConfigurers.java b/application/src/main/java/run/halo/app/security/authorization/AuthorizationExchangeConfigurers.java new file mode 100644 index 000000000..e59f8fec4 --- /dev/null +++ b/application/src/main/java/run/halo/app/security/authorization/AuthorizationExchangeConfigurers.java @@ -0,0 +1,107 @@ +package run.halo.app.security.authorization; + +import java.util.Collections; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.core.user.service.RoleService; +import run.halo.app.security.authentication.SecurityConfigurer; + +/** + * Authorization exchange configurers. + * + * @author johnniang + * @since 2.20.0 + */ +@Component +class AuthorizationExchangeConfigurers { + + private final AuthenticationTrustResolver authenticationTrustResolver = + new AuthenticationTrustResolverImpl(); + + @Bean + @Order(0) + SecurityConfigurer apiAuthorizationConfigurer(RoleService roleService) { + return http -> http.authorizeExchange( + spec -> spec.pathMatchers("/api/**", "/apis/**", "/actuator/**") + .access(new RequestInfoAuthorizationManager(roleService))); + } + + @Bean + @Order(100) + SecurityConfigurer unauthenticatedAuthorizationConfigurer() { + return http -> http.authorizeExchange(spec -> { + spec.pathMatchers(HttpMethod.GET, "/login", "/signup") + .access((authentication, context) -> authentication.map( + a -> !authenticationTrustResolver.isAuthenticated(a) + ) + .defaultIfEmpty(true) + .map(AuthorizationDecision::new)); + }); + } + + @Bean + @Order(200) + SecurityConfigurer preAuthenticationAuthorizationConfigurer() { + return http -> http.authorizeExchange(spec -> spec.pathMatchers( + "/login/**", + "/challenges/**", + "/password-reset/**", + "/signup", + "/logout" + ).permitAll()); + } + + @Bean + @Order(300) + SecurityConfigurer authenticatedAuthorizationConfigurer() { + // Anonymous user is not allowed + return http -> http.authorizeExchange( + spec -> spec.pathMatchers("/console/**", "/uc/**").authenticated() + ); + } + + @Bean + @Order(400) + SecurityConfigurer anonymousOrAuthenticatedAuthorizationConfigurer() { + return http -> http.authorizeExchange( + spec -> spec.matchers(createHtmlMatcher()).access((authentication, context) -> + // we only need to check the authentication is authenticated + // because we treat anonymous user as authenticated + authentication.map(Authentication::isAuthenticated) + .map(AuthorizationDecision::new) + .switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false))) + ) + ); + } + + @Bean + @Order + SecurityConfigurer permitAllAuthorizationConfigurer() { + return http -> http.authorizeExchange(spec -> spec.anyExchange().permitAll()); + } + + private static ServerWebExchangeMatcher createHtmlMatcher() { + ServerWebExchangeMatcher get = + ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**"); + ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher( + ServerWebExchangeMatchers.pathMatchers("/favicon.*")); + MediaTypeServerWebExchangeMatcher html = + new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML); + html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + return new AndServerWebExchangeMatcher(get, notFavicon, html); + } + +} diff --git a/application/src/main/java/run/halo/app/security/authorization/NotAuthenticatedAuthorizationManager.java b/application/src/main/java/run/halo/app/security/authorization/NotAuthenticatedAuthorizationManager.java deleted file mode 100644 index c0435f5d0..000000000 --- a/application/src/main/java/run/halo/app/security/authorization/NotAuthenticatedAuthorizationManager.java +++ /dev/null @@ -1,30 +0,0 @@ -package run.halo.app.security.authorization; - -import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.authentication.AuthenticationTrustResolverImpl; -import org.springframework.security.authorization.AuthorizationDecision; -import org.springframework.security.authorization.ReactiveAuthorizationManager; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.server.authorization.AuthorizationContext; -import reactor.core.publisher.Mono; - -/** - * Authorization manager that checks if the user is not authenticated. - * - * @author johnniang - * @since 2.20.0 - */ -public class NotAuthenticatedAuthorizationManager - implements ReactiveAuthorizationManager { - - private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - - @Override - public Mono check(Mono authentication, - AuthorizationContext object) { - return authentication.map(a -> !trustResolver.isAuthenticated(a)) - .defaultIfEmpty(true) - .map(AuthorizationDecision::new); - } - -}