Fix the problem of getting authentication while rendering error page (#3152)

#### 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
修复错误模板渲染无法获取登录信息的问题
```
pull/3158/head
John Niang 2023-01-16 14:10:12 +08:00 committed by GitHub
parent e03aa4ef72
commit a81380ef8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 6 deletions

View File

@ -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<GrantedAuthority> authorities,
ServerSecurityContextRepository securityContextRepository) {
super(key, principal, authorities);
this.securityContextRepository = securityContextRepository;
}
@Override
public Mono<Void> 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));
}
}

View File

@ -16,10 +16,13 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity; 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.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain; 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.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.RoleService;
@ -45,7 +48,8 @@ public class WebServerSecurityConfig {
@Order(Ordered.HIGHEST_PRECEDENCE) @Order(Ordered.HIGHEST_PRECEDENCE)
SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http, SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
RoleService roleService, RoleService roleService,
ObjectProvider<SecurityConfigurer> securityConfigurers) { ObjectProvider<SecurityConfigurer> securityConfigurers,
ServerSecurityContextRepository securityContextRepository) {
http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/login", "/logout")) http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/login", "/logout"))
.authorizeExchange().anyExchange() .authorizeExchange().anyExchange()
@ -54,6 +58,7 @@ public class WebServerSecurityConfig {
spec.authorities(AnonymousUserConst.Role); spec.authorities(AnonymousUserConst.Role);
spec.principal(AnonymousUserConst.PRINCIPAL); spec.principal(AnonymousUserConst.PRINCIPAL);
}) })
.securityContextRepository(securityContextRepository)
.formLogin(withDefaults()) .formLogin(withDefaults())
.logout(withDefaults()) .logout(withDefaults())
.httpBasic(withDefaults()); .httpBasic(withDefaults());
@ -67,23 +72,30 @@ public class WebServerSecurityConfig {
@Bean @Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1) @Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityWebFilterChain portalFilterChain(ServerHttpSecurity http) { SecurityWebFilterChain portalFilterChain(ServerHttpSecurity http,
ServerSecurityContextRepository securityContextRepository) {
var pathMatcher = pathMatchers(HttpMethod.GET, "/**"); var pathMatcher = pathMatchers(HttpMethod.GET, "/**");
var mediaTypeMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML); var mediaTypeMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
mediaTypeMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL)); mediaTypeMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
http.securityMatcher(new AndServerWebExchangeMatcher(pathMatcher, mediaTypeMatcher)) http.securityMatcher(new AndServerWebExchangeMatcher(pathMatcher, mediaTypeMatcher))
.authorizeExchange().anyExchange().permitAll().and() .authorizeExchange().anyExchange().permitAll().and()
.securityContextRepository(securityContextRepository)
.headers() .headers()
.frameOptions().mode(SAMEORIGIN) .frameOptions().mode(SAMEORIGIN)
.referrerPolicy().policy(STRICT_ORIGIN_WHEN_CROSS_ORIGIN).and() .referrerPolicy().policy(STRICT_ORIGIN_WHEN_CROSS_ORIGIN).and()
.cache().disable().and() .cache().disable().and()
.anonymous(spec -> { .anonymous(spec -> spec.authenticationFilter(
spec.authorities(AnonymousUserConst.Role); new HaloAnonymousAuthenticationWebFilter("portal", AnonymousUserConst.PRINCIPAL,
spec.principal(AnonymousUserConst.PRINCIPAL); AuthorityUtils.createAuthorityList(AnonymousUserConst.Role),
}); securityContextRepository)));
return http.build(); return http.build();
} }
@Bean
ServerSecurityContextRepository securityContextRepository() {
return new WebSessionServerSecurityContextRepository();
}
@Bean @Bean
ReactiveUserDetailsService userDetailsService(UserService userService, ReactiveUserDetailsService userDetailsService(UserService userService,
RoleService roleService) { RoleService roleService) {

View File

@ -12,12 +12,15 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.http.MediaType; 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.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.infra.properties.HaloProperties; import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.utils.FilePathUtils; import run.halo.app.infra.utils.FilePathUtils;
import run.halo.app.theme.dialect.HaloSpringSecurityDialect;
import run.halo.app.theme.dialect.LinkExpressionObjectDialect; import run.halo.app.theme.dialect.LinkExpressionObjectDialect;
/** /**
@ -66,4 +69,10 @@ public class ThemeConfiguration {
LinkExpressionObjectDialect linkExpressionObjectDialect() { LinkExpressionObjectDialect linkExpressionObjectDialect() {
return new LinkExpressionObjectDialect(); return new LinkExpressionObjectDialect();
} }
@Bean
SpringSecurityDialect springSecurityDialect(
ServerSecurityContextRepository securityContextRepository) {
return new HaloSpringSecurityDialect(securityContextRepository);
}
} }

View File

@ -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<String, Object> 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<ServerWebExchange, Object> secCtxInitializer =
(exchange) -> securityContextRepository.load(exchange);
final Function<ServerWebExchange, Object> csrfTokenInitializer =
(exchange) -> {
final Mono<CsrfToken> 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<String, Object> getExecutionAttributes() {
return executionAttributes;
}
}