mirror of https://github.com/halo-dev/halo
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
parent
9bfe3a66d5
commit
bbc5c979b7
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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<SecurityConfigurer> 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String, Object> 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<ServerWebExchange, Object> secCtxInitializer =
|
||||
(exchange) -> securityContextRepository.load(exchange);
|
||||
exchange -> securityContextRepository.load(exchange)
|
||||
.defaultIfEmpty(anonymousSecurityContext);
|
||||
|
||||
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;
|
||||
// Just overwrite the value of the attribute
|
||||
getExecutionAttributes().put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue