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.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.Collections;
|
|
||||||
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,11 +11,8 @@ 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.http.HttpMethod;
|
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.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.Authentication;
|
|
||||||
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;
|
||||||
|
@ -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.context.WebSessionServerSecurityContextRepository;
|
||||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
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.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.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.MapSession;
|
||||||
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
|
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.RoleService;
|
||||||
import run.halo.app.core.user.service.UserService;
|
import run.halo.app.core.user.service.UserService;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
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.PatAuthenticationManager;
|
||||||
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
|
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
|
||||||
import run.halo.app.security.authorization.AuthorityUtils;
|
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.InMemoryReactiveIndexedSessionRepository;
|
||||||
import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
||||||
|
|
||||||
|
@ -86,29 +76,6 @@ public class WebServerSecurityConfig {
|
||||||
new NegatedServerWebExchangeMatcher(staticResourcesMatcher));
|
new NegatedServerWebExchangeMatcher(staticResourcesMatcher));
|
||||||
|
|
||||||
http.securityMatcher(securityMatcher)
|
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 -> {
|
.anonymous(spec -> {
|
||||||
spec.authorities(AuthorityUtils.ROLE_PREFIX + AnonymousUserConst.Role);
|
spec.authorities(AuthorityUtils.ROLE_PREFIX + AnonymousUserConst.Role);
|
||||||
spec.principal(AnonymousUserConst.PRINCIPAL);
|
spec.principal(AnonymousUserConst.PRINCIPAL);
|
||||||
|
@ -190,14 +157,5 @@ public class WebServerSecurityConfig {
|
||||||
return new RsaKeyService(haloProperties.getWorkDir().resolve("keys"));
|
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