Unify security configurations into one (#5961)

#### 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 <https://github.com/halo-dev/halo/pull/3152> may fix <https://github.com/halo-dev/halo/issues/4047>.

#### 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
修复登录成功后立即出现登录失效的问题
```
pull/5967/head^2
John Niang 2024-05-22 11:00:46 +08:00 committed by GitHub
parent 9bfe3a66d5
commit bbc5c979b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 125 deletions

View File

@ -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<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

@ -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.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider; 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.boot.autoconfigure.web.ServerProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.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.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.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; 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.MapSession;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession; import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
@ -60,24 +52,30 @@ import run.halo.app.security.session.ReactiveIndexedSessionRepository;
@RequiredArgsConstructor @RequiredArgsConstructor
public class WebServerSecurityConfig { public class WebServerSecurityConfig {
@Bean(name = "apiSecurityFilterChain") @Bean
@Order(Ordered.HIGHEST_PRECEDENCE) SecurityWebFilterChain filterChain(ServerHttpSecurity http,
SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
RoleService roleService, RoleService roleService,
ObjectProvider<SecurityConfigurer> securityConfigurers, ObjectProvider<SecurityConfigurer> securityConfigurers,
ServerSecurityContextRepository securityContextRepository, ServerSecurityContextRepository securityContextRepository,
ReactiveExtensionClient client, ReactiveExtensionClient client,
PatJwkSupplier patJwkSupplier) { PatJwkSupplier patJwkSupplier,
HaloProperties haloProperties) {
http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/oauth2/**", http.securityMatcher(pathMatchers("/**"))
"/login/**", "/logout", "/actuator/**")) .authorizeExchange(spec -> spec.pathMatchers(
.authorizeExchange(spec -> { "/api/**",
spec.anyExchange().access( "/apis/**",
"/oauth2/**",
"/login/**",
"/logout",
"/actuator/**"
)
.access(
new TwoFactorAuthorizationManager( new TwoFactorAuthorizationManager(
new RequestInfoAuthorizationManager(roleService) new RequestInfoAuthorizationManager(roleService)
) )
); )
}) .anyExchange().permitAll())
.anonymous(spec -> { .anonymous(spec -> {
spec.authorities(AnonymousUserConst.Role); spec.authorities(AnonymousUserConst.Role);
spec.principal(AnonymousUserConst.PRINCIPAL); spec.principal(AnonymousUserConst.PRINCIPAL);
@ -87,33 +85,12 @@ public class WebServerSecurityConfig {
.oauth2ResourceServer(oauth2 -> { .oauth2ResourceServer(oauth2 -> {
var authManagerResolver = builder().add( var authManagerResolver = builder().add(
new PatServerWebExchangeMatcher(), new PatServerWebExchangeMatcher(),
new PatAuthenticationManager(client, patJwkSupplier)) new PatAuthenticationManager(client, patJwkSupplier)
)
// TODO Add other authentication mangers here. e.g.: JwtAuthenticationManager. // TODO Add other authentication mangers here. e.g.: JwtAuthenticationManager.
.build(); .build();
oauth2.authenticationManagerResolver(authManagerResolver); 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 .headers(headerSpec -> headerSpec
.frameOptions(frameSpec -> { .frameOptions(frameSpec -> {
var frameOptions = haloProperties.getSecurity().getFrameOptions(); var frameOptions = haloProperties.getSecurity().getFrameOptions();
@ -122,17 +99,16 @@ public class WebServerSecurityConfig {
frameSpec.disable(); frameSpec.disable();
} }
}) })
.referrerPolicy(referrerPolicySpec -> { .referrerPolicy(referrerPolicySpec -> referrerPolicySpec.policy(
referrerPolicySpec.policy( haloProperties.getSecurity().getReferrerOptions().getPolicy())
haloProperties.getSecurity().getReferrerOptions().getPolicy()); )
})
.cache(ServerHttpSecurity.HeaderSpec.CacheSpec::disable) .cache(ServerHttpSecurity.HeaderSpec.CacheSpec::disable)
.hsts(hstsSpec -> hstsSpec.includeSubdomains(false)) .hsts(hstsSpec -> hstsSpec.includeSubdomains(false))
) );
.anonymous(spec -> spec.authenticationFilter(
new HaloAnonymousAuthenticationWebFilter("portal", AnonymousUserConst.PRINCIPAL, // Integrate with other configurers separately
AuthorityUtils.createAuthorityList(AnonymousUserConst.Role), securityConfigurers.orderedStream()
securityContextRepository))); .forEach(securityConfigurer -> securityConfigurer.configure(http));
return http.build(); return http.build();
} }

View File

@ -1,65 +1,53 @@
package run.halo.app.theme.dialect; package run.halo.app.theme.dialect;
import java.util.HashMap; import static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;
import java.util.Map; import static run.halo.app.infra.AnonymousUserConst.PRINCIPAL;
import static run.halo.app.infra.AnonymousUserConst.Role;
import java.util.function.Function; 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.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect; import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils; import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
import org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils; import org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils;
import reactor.core.publisher.Mono;
/** /**
* HaloSpringSecurityDialect overwrites value of thymeleafSpringSecurityContext. * HaloSpringSecurityDialect overwrites value of thymeleafSpringSecurityContext.
* *
* @author johnniang * @author johnniang
*/ */
public class HaloSpringSecurityDialect extends SpringSecurityDialect { public class HaloSpringSecurityDialect extends SpringSecurityDialect implements InitializingBean {
private static final String SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME = private static final String SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME =
"ThymeleafReactiveModelAdditions:" "ThymeleafReactiveModelAdditions:"
+ SpringSecurityContextUtils.SECURITY_CONTEXT_MODEL_ATTRIBUTE_NAME; + 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; private final ServerSecurityContextRepository securityContextRepository;
public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository) { public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository) {
this.securityContextRepository = securityContextRepository; this.securityContextRepository = securityContextRepository;
executionAttributes = new HashMap<>(3, 1.0f);
initExecutionAttributes();
} }
private void initExecutionAttributes() { @Override
public void afterPropertiesSet() {
if (!SpringVersionUtils.isSpringWebFluxPresent()) { if (!SpringVersionUtils.isSpringWebFluxPresent()) {
return; 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<ServerWebExchange, Object> secCtxInitializer = final Function<ServerWebExchange, Object> secCtxInitializer =
(exchange) -> securityContextRepository.load(exchange); exchange -> securityContextRepository.load(exchange)
.defaultIfEmpty(anonymousSecurityContext);
final Function<ServerWebExchange, Object> csrfTokenInitializer = // Just overwrite the value of the attribute
(exchange) -> { getExecutionAttributes().put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer);
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;
} }
} }