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
John Niang 2024-10-07 23:50:53 +08:00 committed by GitHub
parent c3ecd339a1
commit 9d01b627d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 108 additions and 73 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}