zyro23 / grails-spring-websocket

93 stars 28 forks source link

How to use convertAndSendToUser() with Grails 2.4 #44

Closed caddac closed 7 years ago

caddac commented 7 years ago

I have a Grails 2.4 project using the 1.2x branch of this plugin. I'm using a custom cookie based authentication scheme (not spring security) but I do get the users details in the grails session. I'm trying to understand the best way to send a message directly to a user. I've tried registering a custom handshake handler as described in #35 but haven't had any luck getting that to work. It fails when trying to register the handler.

Does this library support this feature for grails 2.4?

zyro23 commented 7 years ago

grails 2.4.5 is using spring 4.0.9. looks like setHandshakeHandler is already supported in that version: https://github.com/spring-projects/spring-framework/blob/v4.0.9.RELEASE/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java#L37

so registering a custom handshakeHandler, possibly overriding determineUser (https://github.com/spring-projects/spring-framework/blob/v4.0.9.RELEASE/spring-websocket/src/main/java/org/springframework/web/socket/server/support/DefaultHandshakeHandler.java#L348) should work.

without a custom handshake handler, relying on the default principal lookup of DefaultHandshakeHandler, another way would be to add a servlet filter into your filterchain that wraps the request in a HttpServletRequestWrapper and overrides getUserPrincipal() to return a principal that has a name corresponding to your cookie-authenticated username/userid.

spring security does that, too: https://github.com/spring-projects/spring-security/blob/4.0.0.RELEASE/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestWrapper.java#L140

caddac commented 7 years ago

OK, I tried implementing the solution given in #35 but am getting the error:

2017-01-03 14:37:06,292 [localhost-startStop-1] ERROR context.GrailsContextLoaderListener - Error initializing the application: Error creating bean with name 'grailsSimpAnnotationMethodMessageHandler': Cannot resolve reference to bean 'clientInboundChannel' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration.setConfigurers(java.util.List); nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'webSocketConfig': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422]: No default constructor found; nested exception is java.lang.NoSuchMethodException: websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422.() org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'grailsSimpAnnotationMethodMessageHandler': Cannot resolve reference to bean 'clientInboundChannel' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration.setConfigurers(java.util.List); nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'webSocketConfig': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422]: No default constructor found; nested exception is java.lang.NoSuchMethodException: websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422.() at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration.setConfigurers(java.util.List); nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'webSocketConfig': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422]: No default constructor found; nested exception is java.lang.NoSuchMethodException: websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422.() ... 4 more Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration.setConfigurers(java.util.List); nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'webSocketConfig': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422]: No default constructor found; nested exception is java.lang.NoSuchMethodException: websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422.() ... 4 more Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'webSocketConfig': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422]: No default constructor found; nested exception is java.lang.NoSuchMethodException: websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422.() ... 4 more Caused by: org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422]: No default constructor found; nested exception is java.lang.NoSuchMethodException: websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422.() ... 4 more Caused by: java.lang.NoSuchMethodException: websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422.() at java.lang.Class.getConstructor0(Class.java:2892) at java.lang.Class.getDeclaredConstructor(Class.java:2058) ... 4 more

zyro23 commented 7 years ago

you followed the 1.2.x readme? i.e. grails.plugin.springwebsocket.useCustomConfig = true?

and what does your websocketConfig look like? how is it registered? resources.groovy?

zyro23 commented 7 years ago

org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [websocket.WebSocketConfig$$EnhancerBySpringCGLIB$$993b4422]: No default constructor found; looks like component-scan. can you double-check that the websocketConfig you defined now in resource.groovy is not subject to component-scan (e.g. in a package that is not included in spring.bean.packages)?

caddac commented 7 years ago

yes, followed the 1.2x readme including the useCustomConfig.

src\groovy\websocket\WebSocketConfig.groovy

package websocket

import org.h2.engine.User
import org.springframework.context.annotation.Configuration
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServletServerHttpRequest
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.server.support.DefaultHandshakeHandler

import javax.servlet.http.HttpSession
import java.security.Principal;

/**
* Set up our websocket configuration, which uses STOMP, and configure our endpoints
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    ConfigObject config

    /**
     *  using constructor-based injection here because the overridden configuration methods
     *  are called (it seems) before property injection or @PostConstruct handling take place
     */
    WebSocketConfig(ConfigObject config) {
        assert config
        this.config = config
    }

    public class HandshakeHandler extends DefaultHandshakeHandler {
        @Override
        protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
            Principal principal = request.getPrincipal();

            if(principal==null)
            {
                ServletServerHttpRequest req= (ServletServerHttpRequest)request;
                HttpSession session = req.getServletRequest().getSession();
                final User user = (User) session.getAttribute("user");
                principal=new Principal() {
                    @Override
                    public String getName() {
                        return user!=null?user.getId():"";
                    }
                };
            }
            logger.info("principal:{}",principal);
            return principal;
        }
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/queue", "/topic");
        config.setApplicationDestinationPrefixes("/app");
        config.setUserDestinationPrefix "/user/"
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp").setHandshakeHandler(new HandshakeHandler()).withSockJS();
    }
}

resources.groovy

    beans = {
     //other bean registrations removed for brevity

    webSocketConfig websocket.WebSocketConfig

        grailsSimpAnnotationMethodMessageHandler(
           GrailsSimpAnnotationMethodMessageHandler,
          ref("clientInboundChannel"), //this is what seems to be causing the error, should this be some other value?
          ref("clientOutboundChannel"),
          ref("brokerMessagingTemplate")
    ) {
    destinationPrefixes = ["/app"]
    } 
zyro23 commented 7 years ago

oh, please try to remove the constructor from the websocket config. that was only for the plugins default config.

if you write webSocketConfig websocket.WebSocketConfig in resources.groovy, spring does of course try to invoke the default constructor (that is missing). so, no websocket channels were created because websocket config instantiation failed. but those channels are required for the GrailsSimpAnnotationMethodMessageHandler, ofc.

caddac commented 7 years ago

Adding a default constructor to WebsocketsConfig fixed it.

WebSocketConfig(){}

Thanks for all your help. I really appreciate it.