mirror of https://github.com/halo-dev/halo
Customize authorization exchange separately (#6779)
#### What type of PR is this? /kind cleanup /area core /milestone 2.20.x #### What this PR does / why we need it: This PR separates authorization exchange customization into security configurers. I also define the annotations `@Order` on every security configurer in order to customize authorization exchange in separated source file instead of modifying existing. #### Does this PR introduce a user-facing change? ```release-note None ```pull/6780/head
parent
c3ecd339a1
commit
9d01b627d0
|
@ -3,7 +3,6 @@ package run.halo.app.infra.config;
|
|||
import static org.springframework.security.web.server.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder;
|
||||
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
@ -12,11 +11,8 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
|
|||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
|
@ -24,13 +20,9 @@ import org.springframework.security.web.server.context.ServerSecurityContextRepo
|
|||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||
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.NegatedServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.RoleService;
|
||||
import run.halo.app.core.user.service.UserService;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -44,8 +36,6 @@ import run.halo.app.security.authentication.impl.RsaKeyService;
|
|||
import run.halo.app.security.authentication.pat.PatAuthenticationManager;
|
||||
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
|
||||
import run.halo.app.security.authorization.AuthorityUtils;
|
||||
import run.halo.app.security.authorization.NotAuthenticatedAuthorizationManager;
|
||||
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
|
||||
import run.halo.app.security.session.InMemoryReactiveIndexedSessionRepository;
|
||||
import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
||||
|
||||
|
@ -86,29 +76,6 @@ public class WebServerSecurityConfig {
|
|||
new NegatedServerWebExchangeMatcher(staticResourcesMatcher));
|
||||
|
||||
http.securityMatcher(securityMatcher)
|
||||
.authorizeExchange(spec -> spec.pathMatchers(
|
||||
"/api/**",
|
||||
"/apis/**",
|
||||
"/actuator/**"
|
||||
).access(new RequestInfoAuthorizationManager(roleService))
|
||||
.pathMatchers(HttpMethod.GET, "/login", "/signup")
|
||||
.access(new NotAuthenticatedAuthorizationManager())
|
||||
.pathMatchers(
|
||||
"/login/**",
|
||||
"/challenges/**",
|
||||
"/password-reset/**",
|
||||
"/signup",
|
||||
"/logout"
|
||||
).permitAll()
|
||||
.pathMatchers("/console/**", "/uc/**").authenticated()
|
||||
.matchers(createHtmlMatcher()).access((authentication, context) ->
|
||||
// we only need to check the authentication is authenticated
|
||||
// because we treat anonymous user as authenticated
|
||||
authentication.map(Authentication::isAuthenticated)
|
||||
.map(AuthorizationDecision::new)
|
||||
.switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))
|
||||
)
|
||||
.anyExchange().permitAll())
|
||||
.anonymous(spec -> {
|
||||
spec.authorities(AuthorityUtils.ROLE_PREFIX + AnonymousUserConst.Role);
|
||||
spec.principal(AnonymousUserConst.PRINCIPAL);
|
||||
|
@ -190,14 +157,5 @@ public class WebServerSecurityConfig {
|
|||
return new RsaKeyService(haloProperties.getWorkDir().resolve("keys"));
|
||||
}
|
||||
|
||||
private static ServerWebExchangeMatcher createHtmlMatcher() {
|
||||
ServerWebExchangeMatcher get =
|
||||
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
|
||||
ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(
|
||||
ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
|
||||
MediaTypeServerWebExchangeMatcher html =
|
||||
new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
||||
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
return new AndServerWebExchangeMatcher(get, notFavicon, html);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.NegatedServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.RoleService;
|
||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||
|
||||
/**
|
||||
* Authorization exchange configurers.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
@Component
|
||||
class AuthorizationExchangeConfigurers {
|
||||
|
||||
private final AuthenticationTrustResolver authenticationTrustResolver =
|
||||
new AuthenticationTrustResolverImpl();
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
SecurityConfigurer apiAuthorizationConfigurer(RoleService roleService) {
|
||||
return http -> http.authorizeExchange(
|
||||
spec -> spec.pathMatchers("/api/**", "/apis/**", "/actuator/**")
|
||||
.access(new RequestInfoAuthorizationManager(roleService)));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
SecurityConfigurer unauthenticatedAuthorizationConfigurer() {
|
||||
return http -> http.authorizeExchange(spec -> {
|
||||
spec.pathMatchers(HttpMethod.GET, "/login", "/signup")
|
||||
.access((authentication, context) -> authentication.map(
|
||||
a -> !authenticationTrustResolver.isAuthenticated(a)
|
||||
)
|
||||
.defaultIfEmpty(true)
|
||||
.map(AuthorizationDecision::new));
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
SecurityConfigurer preAuthenticationAuthorizationConfigurer() {
|
||||
return http -> http.authorizeExchange(spec -> spec.pathMatchers(
|
||||
"/login/**",
|
||||
"/challenges/**",
|
||||
"/password-reset/**",
|
||||
"/signup",
|
||||
"/logout"
|
||||
).permitAll());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(300)
|
||||
SecurityConfigurer authenticatedAuthorizationConfigurer() {
|
||||
// Anonymous user is not allowed
|
||||
return http -> http.authorizeExchange(
|
||||
spec -> spec.pathMatchers("/console/**", "/uc/**").authenticated()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(400)
|
||||
SecurityConfigurer anonymousOrAuthenticatedAuthorizationConfigurer() {
|
||||
return http -> http.authorizeExchange(
|
||||
spec -> spec.matchers(createHtmlMatcher()).access((authentication, context) ->
|
||||
// we only need to check the authentication is authenticated
|
||||
// because we treat anonymous user as authenticated
|
||||
authentication.map(Authentication::isAuthenticated)
|
||||
.map(AuthorizationDecision::new)
|
||||
.switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order
|
||||
SecurityConfigurer permitAllAuthorizationConfigurer() {
|
||||
return http -> http.authorizeExchange(spec -> spec.anyExchange().permitAll());
|
||||
}
|
||||
|
||||
private static ServerWebExchangeMatcher createHtmlMatcher() {
|
||||
ServerWebExchangeMatcher get =
|
||||
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
|
||||
ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(
|
||||
ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
|
||||
MediaTypeServerWebExchangeMatcher html =
|
||||
new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
||||
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
return new AndServerWebExchangeMatcher(get, notFavicon, html);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package run.halo.app.security.authorization;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.authorization.AuthorizationContext;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Authorization manager that checks if the user is not authenticated.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public class NotAuthenticatedAuthorizationManager
|
||||
implements ReactiveAuthorizationManager<AuthorizationContext> {
|
||||
|
||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
@Override
|
||||
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication,
|
||||
AuthorizationContext object) {
|
||||
return authentication.map(a -> !trustResolver.isAuthenticated(a))
|
||||
.defaultIfEmpty(true)
|
||||
.map(AuthorizationDecision::new);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue