From bbc5c979b73da9c29412b764b1837ae3dd4b2373 Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 22 May 2024 11:00:46 +0800 Subject: [PATCH] Unify security configurations into one (#5961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.16.x #### What this PR does / why we need it: This PR unifies api and portal security configurations into one for a better maintenance. Meanwhile, removing `HaloAnonymousAuthenticationWebFilter` introduced by may fix . #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/4047 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 修复登录成功后立即出现登录失效的问题 ``` --- .../HaloAnonymousAuthenticationWebFilter.java | 43 ----------- .../app/config/WebServerSecurityConfig.java | 76 +++++++------------ .../dialect/HaloSpringSecurityDialect.java | 52 +++++-------- 3 files changed, 46 insertions(+), 125 deletions(-) delete mode 100644 application/src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java diff --git a/application/src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java b/application/src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java deleted file mode 100644 index 5c04bdd0c..000000000 --- a/application/src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -package run.halo.app.config; - -import static org.springframework.security.core.context.ReactiveSecurityContextHolder.withSecurityContext; - -import java.util.List; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import org.springframework.security.core.context.SecurityContextImpl; -import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter; -import org.springframework.security.web.server.context.ServerSecurityContextRepository; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilterChain; -import reactor.core.publisher.Mono; - -/** - * HaloAnonymousAuthenticationWebFilter will save SecurityContext into SecurityContextRepository - * when AnonymousAuthenticationToken is created. - * - * @author johnniang - */ -public class HaloAnonymousAuthenticationWebFilter extends AnonymousAuthenticationWebFilter { - - private final ServerSecurityContextRepository securityContextRepository; - - public HaloAnonymousAuthenticationWebFilter(String key, Object principal, - List authorities, - ServerSecurityContextRepository securityContextRepository) { - super(key, principal, authorities); - this.securityContextRepository = securityContextRepository; - } - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return ReactiveSecurityContextHolder.getContext().switchIfEmpty(Mono.defer(() -> { - var authentication = createAuthentication(exchange); - var securityContext = new SecurityContextImpl(authentication); - return securityContextRepository.save(exchange, securityContext) - .then(Mono.defer(() -> chain.filter(exchange) - .contextWrite(withSecurityContext(Mono.just(securityContext))) - .then(Mono.empty()))); - })).flatMap(securityContext -> chain.filter(exchange)); - } -} diff --git a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java index 28e3d00a1..1c476a5b4 100644 --- a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java +++ b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java @@ -4,7 +4,6 @@ import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.web.server.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder; import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.ObjectProvider; @@ -12,20 +11,13 @@ import org.springframework.boot.autoconfigure.session.SessionProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; -import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; import org.springframework.session.MapSession; import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; import org.springframework.web.reactive.function.server.RouterFunction; @@ -60,24 +52,30 @@ import run.halo.app.security.session.ReactiveIndexedSessionRepository; @RequiredArgsConstructor public class WebServerSecurityConfig { - @Bean(name = "apiSecurityFilterChain") - @Order(Ordered.HIGHEST_PRECEDENCE) - SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http, + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http, RoleService roleService, ObjectProvider securityConfigurers, ServerSecurityContextRepository securityContextRepository, ReactiveExtensionClient client, - PatJwkSupplier patJwkSupplier) { + PatJwkSupplier patJwkSupplier, + HaloProperties haloProperties) { - http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/oauth2/**", - "/login/**", "/logout", "/actuator/**")) - .authorizeExchange(spec -> { - spec.anyExchange().access( + http.securityMatcher(pathMatchers("/**")) + .authorizeExchange(spec -> spec.pathMatchers( + "/api/**", + "/apis/**", + "/oauth2/**", + "/login/**", + "/logout", + "/actuator/**" + ) + .access( new TwoFactorAuthorizationManager( new RequestInfoAuthorizationManager(roleService) ) - ); - }) + ) + .anyExchange().permitAll()) .anonymous(spec -> { spec.authorities(AnonymousUserConst.Role); spec.principal(AnonymousUserConst.PRINCIPAL); @@ -87,33 +85,12 @@ public class WebServerSecurityConfig { .oauth2ResourceServer(oauth2 -> { var authManagerResolver = builder().add( new PatServerWebExchangeMatcher(), - new PatAuthenticationManager(client, patJwkSupplier)) + new PatAuthenticationManager(client, patJwkSupplier) + ) // TODO Add other authentication mangers here. e.g.: JwtAuthenticationManager. .build(); oauth2.authenticationManagerResolver(authManagerResolver); }) - .headers(headerSpec -> headerSpec.hsts(hstsSpec -> hstsSpec.includeSubdomains(false))) - ; - - // Integrate with other configurers separately - securityConfigurers.orderedStream() - .forEach(securityConfigurer -> securityConfigurer.configure(http)); - return http.build(); - } - - @Bean - @Order(Ordered.HIGHEST_PRECEDENCE + 1) - SecurityWebFilterChain portalFilterChain(ServerHttpSecurity http, - ServerSecurityContextRepository securityContextRepository, - HaloProperties haloProperties) { - var pathMatcher = pathMatchers(HttpMethod.GET, "/**"); - var mediaTypeMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML); - mediaTypeMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL)); - http.securityMatcher(new AndServerWebExchangeMatcher(pathMatcher, mediaTypeMatcher)) - .securityContextRepository(securityContextRepository) - .authorizeExchange(spec -> { - spec.anyExchange().permitAll(); - }) .headers(headerSpec -> headerSpec .frameOptions(frameSpec -> { var frameOptions = haloProperties.getSecurity().getFrameOptions(); @@ -122,17 +99,16 @@ public class WebServerSecurityConfig { frameSpec.disable(); } }) - .referrerPolicy(referrerPolicySpec -> { - referrerPolicySpec.policy( - haloProperties.getSecurity().getReferrerOptions().getPolicy()); - }) + .referrerPolicy(referrerPolicySpec -> referrerPolicySpec.policy( + haloProperties.getSecurity().getReferrerOptions().getPolicy()) + ) .cache(ServerHttpSecurity.HeaderSpec.CacheSpec::disable) .hsts(hstsSpec -> hstsSpec.includeSubdomains(false)) - ) - .anonymous(spec -> spec.authenticationFilter( - new HaloAnonymousAuthenticationWebFilter("portal", AnonymousUserConst.PRINCIPAL, - AuthorityUtils.createAuthorityList(AnonymousUserConst.Role), - securityContextRepository))); + ); + + // Integrate with other configurers separately + securityConfigurers.orderedStream() + .forEach(securityConfigurer -> securityConfigurer.configure(http)); return http.build(); } diff --git a/application/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java b/application/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java index b9f91b040..31f2b623a 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java +++ b/application/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java @@ -1,65 +1,53 @@ package run.halo.app.theme.dialect; -import java.util.HashMap; -import java.util.Map; +import static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList; +import static run.halo.app.infra.AnonymousUserConst.PRINCIPAL; +import static run.halo.app.infra.AnonymousUserConst.Role; + import java.util.function.Function; -import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.web.server.context.ServerSecurityContextRepository; -import org.springframework.security.web.server.csrf.CsrfToken; import org.springframework.web.server.ServerWebExchange; import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect; import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils; import org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils; -import reactor.core.publisher.Mono; /** * HaloSpringSecurityDialect overwrites value of thymeleafSpringSecurityContext. * * @author johnniang */ -public class HaloSpringSecurityDialect extends SpringSecurityDialect { +public class HaloSpringSecurityDialect extends SpringSecurityDialect implements InitializingBean { private static final String SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME = "ThymeleafReactiveModelAdditions:" + SpringSecurityContextUtils.SECURITY_CONTEXT_MODEL_ATTRIBUTE_NAME; - private static final String CSRF_EXECUTION_ATTRIBUTE_NAME = - "ThymeleafReactiveModelAdditions:" + CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME; - - private final Map executionAttributes; private final ServerSecurityContextRepository securityContextRepository; public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository) { this.securityContextRepository = securityContextRepository; - executionAttributes = new HashMap<>(3, 1.0f); - initExecutionAttributes(); } - private void initExecutionAttributes() { + @Override + public void afterPropertiesSet() { if (!SpringVersionUtils.isSpringWebFluxPresent()) { return; } + // We have to build an anonymous authentication token here because the token won't be saved + // into repository during anonymous authentication. + var anonymousAuthentication = + new AnonymousAuthenticationToken("fallback", PRINCIPAL, createAuthorityList(Role)); + var anonymousSecurityContext = new SecurityContextImpl(anonymousAuthentication); + final Function secCtxInitializer = - (exchange) -> securityContextRepository.load(exchange); + exchange -> securityContextRepository.load(exchange) + .defaultIfEmpty(anonymousSecurityContext); - final Function csrfTokenInitializer = - (exchange) -> { - final Mono csrfToken = exchange.getAttribute(CsrfToken.class.getName()); - if (csrfToken == null) { - return Mono.empty(); - } - return csrfToken.doOnSuccess( - token -> exchange.getAttributes() - .put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token)); - }; - - executionAttributes.put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer); - executionAttributes.put(CSRF_EXECUTION_ATTRIBUTE_NAME, csrfTokenInitializer); - } - - @Override - public Map getExecutionAttributes() { - return executionAttributes; + // Just overwrite the value of the attribute + getExecutionAttributes().put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer); } }