mirror of https://github.com/halo-dev/halo
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  #### Does this PR introduce a user-facing change? ```release-note 修复错误模板渲染无法获取登录信息的问题 ```pull/3158/head
parent
e03aa4ef72
commit
a81380ef8e
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue