graphql-spqr-spring-boot-starter icon indicating copy to clipboard operation
graphql-spqr-spring-boot-starter copied to clipboard

SpqrWebSocketAutoConfiguration is too specific

Open iamlothian opened this issue 5 years ago • 1 comments

Firstly, great work so far on this package.

The SpqrWebSocketAutoConfiguration defines a number of beans used to set up WebSocket handlers. I need to decorate the default WebSocketHandler to add extra functionality, without reimplementing it.

My attempt at a configuration class to override the SpqrWebSocketAutoConfiguration:

@Configuration
public class WebSocketSecurityConfig {
    /**
     * security context for webSocket connections
     * @return
     */
    @Bean
    public WebSocketContextFactory webSocketContextFactory() {
        return params -> new WebSocketSecurityGlobalContext(params.getNativeRequest());
    }
    /**
     * Authorized webSocketExecutor
     * @param contextFactory
     * @param dataLoaderRegistryFactory
     * @return
     */
    @Bean
    public GraphQLWebSocketExecutor webSocketExecutor(
            WebSocketContextFactory contextFactory,
            @Autowired(required = false) DataLoaderRegistryFactory dataLoaderRegistryFactory
    ) {
        return new AuthorizedGraphQLWebSocketExecutor(contextFactory, dataLoaderRegistryFactory);
    }
    /**
     * FAILS:  won't work as the `webSocketHandler` expects type `PerConnectionApolloHandler` when `WebSocketHandler` would work just as well here.
     * Decorated PerConnectionApolloHandler which extracts and validates BearerToken during the lifecycle of the connection
     * @param apolloWebSocketHandler
     * @return
     */
    @Bean
    public WebSocketHandler webSocketHandler(PerConnectionApolloHandler apolloWebSocketHandler) {
        return new BearerTokenWebSocketHandler(apolloWebSocketHandler);
    }

    // copied out of SpqrWebSocketAutoConfiguration due to poor abstraction of WebSocketHandler Bean
    @Bean
    public PerConnectionApolloHandler apolloWebSocketHandler(GraphQLWebSocketExecutor executor, GraphQL graphQL, SpqrProperties config) {
        boolean keepAliveEnabled = config.getWs().getKeepAlive().isEnabled();
        int keepAliveInterval = config.getWs().getKeepAlive().getIntervalMillis();
        return new PerConnectionApolloHandler(graphQL, executor,
                keepAliveEnabled ? defaultTaskScheduler() : null, keepAliveInterval);
    }

    // copied out of SpqrWebSocketAutoConfiguration due to poor abstraction of WebSocketHandler Bean
    private TaskScheduler defaultTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolScheduler = new ThreadPoolTaskScheduler();
        threadPoolScheduler.setThreadNamePrefix("GraphQLWSKeepAlive-");
        threadPoolScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
        threadPoolScheduler.setRemoveOnCancelPolicy(true);
        threadPoolScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        threadPoolScheduler.initialize();
        return threadPoolScheduler;
    }

}

I think customization of this could be simplified by providing better access to the webSocketHandlerRegistry to configure the handlers and interceptors, and separating the GraphQL and SpqrProperties logic out from the ConditionalOnMissingBean definitions so it can be reused if you only want to extend the existing configuration.

If I find time I'll make a PR with some of the above for your consideration.

iamlothian avatar May 19 '20 22:05 iamlothian

As a workaround, I have to reimplement and disable SpqrWebSocketAutoConfiguration

like so

/**
 * Reimplemented the io.leangen.graphql.spqr.spring.autoconfigure.SpqrWebSocketAutoConfiguration
 * make sure to set the property "graphql.spqr.ws.enabled" to false
 */
@Configuration
@EnableWebSocket
@Slf4j
public class WebSocketSecurityConfigurer implements WebSocketConfigurer {

    private final GraphQL graphQL;
    private final SpqrProperties config;
    private final DataLoaderRegistryFactory dataLoaderRegistryFactory;

    @Autowired
    @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
    public WebSocketSecurityConfigurer(GraphQL graphQL, SpqrProperties config,
                                       Optional<DataLoaderRegistryFactory> dataLoaderRegistryFactory) {
        this.graphQL = graphQL;
        this.config = config;
        this.dataLoaderRegistryFactory = dataLoaderRegistryFactory.orElse(null);
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        String webSocketEndpoint = config.getWs().getEndpoint();
        String graphQLEndpoint = config.getHttp().getEndpoint();
        String endpointUrl = webSocketEndpoint == null ? graphQLEndpoint : webSocketEndpoint;

        boolean keepAliveEnabled = config.getWs().getKeepAlive().isEnabled();
        int keepAliveInterval = config.getWs().getKeepAlive().getIntervalMillis();

        SecuredWebSocketContextFactory connectionFactory = new SecuredWebSocketContextFactory();
        AuthorizedGraphQLWebSocketExecutor executor = new AuthorizedGraphQLWebSocketExecutor(connectionFactory, dataLoaderRegistryFactory);
        WebSocketHandler apolloHandler = new PerConnectionApolloHandler(graphQL, executor, keepAliveEnabled ? defaultTaskScheduler() : null, keepAliveInterval);
        WebSocketHandler securityHandler = new BearerTokenWebSocketHandler(apolloHandler);

        webSocketHandlerRegistry
                .addHandler(securityHandler, endpointUrl)
                .setAllowedOrigins(config.getWs().getAllowedOrigins());
    }

    private TaskScheduler defaultTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolScheduler = new ThreadPoolTaskScheduler();
        threadPoolScheduler.setThreadNamePrefix("GraphQLWSKeepAlive-");
        threadPoolScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
        threadPoolScheduler.setRemoveOnCancelPolicy(true);
        threadPoolScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        threadPoolScheduler.initialize();
        return threadPoolScheduler;
    }
}

iamlothian avatar May 19 '20 22:05 iamlothian