Modularizing Spring Security SecurityWebFilterChain

Viewed 1140

Our Spring Security configuration file is growing to big and we would like to break it apart into smaller parts. Right now we have the following:

public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    http.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/**"))
            .authenticationManager(this.authenticationManager);

    http.authorizeExchange()
            .pathMatchers(HttpMethod.GET, "/api/serviceA/**")
            .hasAuthority("PROP_A");

    http.authorizeExchange()
            .pathMatchers(HttpMethod.GET, "/api/serviceB/**")
            .hasAuthority("PROP_B");

    http.authorizeExchange().pathMatchers(HttpMethod.POST, "/api/login", "/api/logout", "/api/forgotPassword", "/api/confirmForgotPassword").permitAll();

    http.csrf()
            .disable()
            .formLogin()
            .authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
            .requiresAuthenticationMatcher(
                    ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/api/login"))
            .authenticationFailureHandler(CustomSpringSecurity::onAuthenticationFailure)
            .authenticationSuccessHandler(CustomSpringSecurity::onAuthenticationSuccess)
            .and()
            .logout()
            .logoutUrl("/api/logout")
            .logoutSuccessHandler(new CustomLogoutSuccessHandler(HttpStatus.OK));

    final SecurityWebFilterChain build = http.build();

    build
            .getWebFilters()
            .collectList()
            .subscribe(
                    webFilters -> {
                        for (WebFilter filter : webFilters) {
                            if (filter instanceof AuthenticationWebFilter) {
                                AuthenticationWebFilter awf = (AuthenticationWebFilter) filter;
                                awf.setServerAuthenticationConverter(CustomSpringSecurity::convert);
                            }
                        }
                    });

    return build;
}

We would like to use securityMatcher to break out /api/seviceA/** and /api/seviceB/** to there own SecurityWebFilterChain @Beans.

However the issue we have is the extra amout of configuration that exist in the configuration. We would like the end result to look like the following.

    public SecurityWebFilterChain securityWebFilterChainForServiceA(ServerHttpSecurity http) {
        http.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/serviceA/**"));

        http.authorizeExchange()
                .pathMatchers(HttpMethod.GET, "/api/serviceA/**")
                .hasAuthority("PROP_A");
        return http.build();
    }

And we would like all the other configuration to be implicit for the endpoint.

How would it be possible to make such a modularization in Spring Security?

1 Answers

You can specify an interface like so:

    public interface HttpSecurityConfig {
        Consumer<ServerHttpSecurity> configuration();
    }

Then create a class that implements this for each of your endpoints which you can inject as beans:

    @Component
    public class ServiceASecurityConfig implements HttpSecurityConfig {
        @Override
        public Consumer<ServerHttpSecurity> configuration() {
            return (http) -> {

                http.authorizeExchange()
                        .pathMatchers(HttpMethod.GET, "/api/serviceA/**")
                        .hasAuthority("PROP_A");
            };
        }
    }

    @Component
    public class ServiceBSecurityConfig implements HttpSecurityConfig {
        @Override
        public Consumer<ServerHttpSecurity> configuration() {
            return (http) -> {

                http.authorizeExchange()
                        .pathMatchers(HttpMethod.GET, "/api/serviceB/**")
                        .hasAuthority("PROP_B");
            };
        }
    }

And finally amend your SecurityWebFilterChain so it injects all the beans of type HttpSecurityConfig and applies the configuration, something like this:

public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, final List<HttpSecurityConfig> httpConfigurations) {
    http.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/**"))
            .authenticationManager(this.authenticationManager);

    // This line replaces the individual configurations in your original question
    httpConfigurations.forEach(config -> config.configuration().accept(http));

    http.authorizeExchange().pathMatchers(HttpMethod.POST, "/api/login", "/api/logout", "/api/forgotPassword", "/api/confirmForgotPassword").permitAll();

    http.csrf()
            .disable()
            .formLogin()
            .authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
            .requiresAuthenticationMatcher(
                    ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/api/login"))
            .authenticationFailureHandler(CustomSpringSecurity::onAuthenticationFailure)
            .authenticationSuccessHandler(CustomSpringSecurity::onAuthenticationSuccess)
            .and()
            .logout()
            .logoutUrl("/api/logout")
            .logoutSuccessHandler(new CustomLogoutSuccessHandler(HttpStatus.OK));

    final SecurityWebFilterChain build = http.build();

    build
            .getWebFilters()
            .collectList()
            .subscribe(
                    webFilters -> {
                        for (WebFilter filter : webFilters) {
                            if (filter instanceof AuthenticationWebFilter) {
                                AuthenticationWebFilter awf = (AuthenticationWebFilter) filter;
                                awf.setServerAuthenticationConverter(CustomSpringSecurity::convert);
                            }
                        }
                    });

    return build;
}
Related