From a81380ef8ef65dc51f56be41096fb9aa87b675a5 Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 16 Jan 2023 14:10:12 +0800 Subject: [PATCH] Fix the problem of getting authentication while rendering error page (#3152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind bug /area core /milestone 2.2.x #### What this PR does / why we need it: This PR mainly customizes AnonymousAuthenticationWebFilter and SpringSecurityDialect to make authentication visible in exception web handler. So that we can get SecurityContext while rendering error page. #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3133 #### Test steps 1. Start Halo 2. Request a page which is not found 3. See upper right corner icon ![image](https://user-images.githubusercontent.com/16865714/212372504-b489507a-2d85-4f1a-a210-9653ad2fa8c2.png) #### Does this PR introduce a user-facing change? ```release-note 修复错误模板渲染无法获取登录信息的问题 ``` --- .../HaloAnonymousAuthenticationWebFilter.java | 43 ++++++++++++ .../app/config/WebServerSecurityConfig.java | 24 +++++-- .../halo/app/theme/ThemeConfiguration.java | 9 +++ .../dialect/HaloSpringSecurityDialect.java | 65 +++++++++++++++++++ 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java create mode 100644 src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java diff --git a/src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java b/src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java new file mode 100644 index 000000000..5c04bdd0c --- /dev/null +++ b/src/main/java/run/halo/app/config/HaloAnonymousAuthenticationWebFilter.java @@ -0,0 +1,43 @@ +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/src/main/java/run/halo/app/config/WebServerSecurityConfig.java b/src/main/java/run/halo/app/config/WebServerSecurityConfig.java index 928600f8e..c832f4c80 100644 --- a/src/main/java/run/halo/app/config/WebServerSecurityConfig.java +++ b/src/main/java/run/halo/app/config/WebServerSecurityConfig.java @@ -16,10 +16,13 @@ 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.core.userdetails.ReactiveUserDetailsService; 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 run.halo.app.core.extension.service.RoleService; @@ -45,7 +48,8 @@ public class WebServerSecurityConfig { @Order(Ordered.HIGHEST_PRECEDENCE) SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http, RoleService roleService, - ObjectProvider securityConfigurers) { + ObjectProvider securityConfigurers, + ServerSecurityContextRepository securityContextRepository) { http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/login", "/logout")) .authorizeExchange().anyExchange() @@ -54,6 +58,7 @@ public class WebServerSecurityConfig { spec.authorities(AnonymousUserConst.Role); spec.principal(AnonymousUserConst.PRINCIPAL); }) + .securityContextRepository(securityContextRepository) .formLogin(withDefaults()) .logout(withDefaults()) .httpBasic(withDefaults()); @@ -67,23 +72,30 @@ public class WebServerSecurityConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE + 1) - SecurityWebFilterChain portalFilterChain(ServerHttpSecurity http) { + SecurityWebFilterChain portalFilterChain(ServerHttpSecurity http, + ServerSecurityContextRepository securityContextRepository) { var pathMatcher = pathMatchers(HttpMethod.GET, "/**"); var mediaTypeMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML); mediaTypeMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL)); http.securityMatcher(new AndServerWebExchangeMatcher(pathMatcher, mediaTypeMatcher)) .authorizeExchange().anyExchange().permitAll().and() + .securityContextRepository(securityContextRepository) .headers() .frameOptions().mode(SAMEORIGIN) .referrerPolicy().policy(STRICT_ORIGIN_WHEN_CROSS_ORIGIN).and() .cache().disable().and() - .anonymous(spec -> { - spec.authorities(AnonymousUserConst.Role); - spec.principal(AnonymousUserConst.PRINCIPAL); - }); + .anonymous(spec -> spec.authenticationFilter( + new HaloAnonymousAuthenticationWebFilter("portal", AnonymousUserConst.PRINCIPAL, + AuthorityUtils.createAuthorityList(AnonymousUserConst.Role), + securityContextRepository))); return http.build(); } + @Bean + ServerSecurityContextRepository securityContextRepository() { + return new WebSessionServerSecurityContextRepository(); + } + @Bean ReactiveUserDetailsService userDetailsService(UserService userService, RoleService roleService) { diff --git a/src/main/java/run/halo/app/theme/ThemeConfiguration.java b/src/main/java/run/halo/app/theme/ThemeConfiguration.java index c63a140c8..9a92c53ca 100644 --- a/src/main/java/run/halo/app/theme/ThemeConfiguration.java +++ b/src/main/java/run/halo/app/theme/ThemeConfiguration.java @@ -12,12 +12,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.FileSystemResource; import org.springframework.http.MediaType; +import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; +import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect; import reactor.core.publisher.Mono; import run.halo.app.infra.properties.HaloProperties; import run.halo.app.infra.utils.FilePathUtils; +import run.halo.app.theme.dialect.HaloSpringSecurityDialect; import run.halo.app.theme.dialect.LinkExpressionObjectDialect; /** @@ -66,4 +69,10 @@ public class ThemeConfiguration { LinkExpressionObjectDialect linkExpressionObjectDialect() { return new LinkExpressionObjectDialect(); } + + @Bean + SpringSecurityDialect springSecurityDialect( + ServerSecurityContextRepository securityContextRepository) { + return new HaloSpringSecurityDialect(securityContextRepository); + } } diff --git a/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java b/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java new file mode 100644 index 000000000..b9f91b040 --- /dev/null +++ b/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java @@ -0,0 +1,65 @@ +package run.halo.app.theme.dialect; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; +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 { + + 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() { + if (!SpringVersionUtils.isSpringWebFluxPresent()) { + return; + } + + final Function secCtxInitializer = + (exchange) -> securityContextRepository.load(exchange); + + 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; + } +}