websockets starter example spring websocket stomp sockjs spring-websocket

starter - Spring 4 AbstractWebSocketMessageBrokerConfigurer con SockJS no negocia correctamente el transporte



websocket spring boot angular 4 (3)

Así que debo decir que todos los ejemplos / tutoriales de websocket parecen ser tan fáciles, pero parece que realmente hay que buscar para encontrar piezas de información realmente importantes que quedan fuera de los ejemplos simples. Todavía tengo bastantes problemas con mi aplicación web usando el intermediario de mensajes Spring 4 Stomp con SockJS en la interfaz.

Actualmente, si agrego un punto final al StompEndpointRegistry sin habilitar SockJS (), entonces declaro mi socket en la interfaz utilizando el dojox / socket de dojo, Firefox 28 abrirá un websocket muy bien. Sin embargo, necesito soporte en IE8 e IE9, así que cambié a SockJS. Usando AbstractAnnotationConfigDispatcherServletInitializer, me llevó bastante tiempo averiguar cómo asegurar que todos los filtros y servlets se configuraran para usar async (para esto, la documentación es muy escasa). Una vez que resolví esto, ahora puedo hacer que funcione en Firefox, pero solo usando xhr_streaming. Con sessionCookieNeeded establecido en true, IE9 se predetermina a intentar usar iframes para la conexión, sin embargo, falla:

LOG: Opening Web Socket... LOG: Opening transport: iframe-htmlfile url:rest/hello/904/ft3apk1g RTO:1008 LOG: Closed transport: iframe-htmlfile SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) LOG: Opening transport: iframe-xhr-polling url:rest/hello/904/bf63eisu RTO:1008 LOG: Closed transport: iframe-xhr-polling SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) LOG: Whoops! Lost connection to undefined

si configuro la cookie necesaria en falso, IE usará xdr-streaming y funcionará bien, sin embargo, pierde la cookie jsessionid en las solicitudes y, a su vez, pierdo la capacidad de adquirir el Principal en el controlador, lo cual es importante para mí. He habilitado el mismo origen x encabezados de trama en la seguridad de primavera y he verificado que los encabezados están presentes en las solicitudes, pero no ayudó. Así que me gustaría poder averiguar cómo A) hacer que Spring y SockJS negocien adecuadamente utilizando el transporte de WebSocket en Firefox, y B) obtener IE8 y 9 para que utilicen adecuadamente el transporte de iframes para que pueda conservar las cookies.

Aquí está mi config / código:

Configuración de la aplicación web:

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { super.onStartup(servletContext); Map<String, ? extends FilterRegistration> registrations = servletContext.getFilterRegistrations(); } @Override protected void customizeRegistration(ServletRegistration.Dynamic registration) { // this is needed for async support for websockets/sockjs registration.setInitParameter("dispatchOptionsRequest", "true"); registration.setAsyncSupported(true); } @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{SecurityConfig.class, Log4jConfig.class, PersistenceConfig.class, ServiceConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { // loading the Initializer class from the dispatcher servlet context ensures it only executes once, // as the ContextRefreshedEvent fires once from the root context and once from the dispatcher servlet context return new Class[]{SpringMvcConfig.class, WebSocketConfig.class}; } @Override protected String[] getServletMappings() { return new String[]{ "/rest/*", "/index.html", "/login.html", "/admin.html", "/index/*", "/login/*", "/admin/*" }; } @Override protected Filter[] getServletFilters() { OpenEntityManagerInViewFilter openEntityManagerInViewFilter = new OpenEntityManagerInViewFilter(); openEntityManagerInViewFilter.setBeanName("openEntityManagerInViewFilter"); openEntityManagerInViewFilter.setPersistenceUnitName("HSQL"); CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter(); encodingFilter.setEncoding("UTF-8"); encodingFilter.setForceEncoding(true); return new javax.servlet.Filter[]{openEntityManagerInViewFilter, encodingFilter}; } }

Spring MVC config:

@Configuration @EnableWebMvc @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) @ComponentScan(basePackages = "x.controllers") // Only scan for controllers. Other classes are scanned in the parent''s root context public class SpringMvcConfig extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926); registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926); registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(mappingJacksonHttpMessageConverter()); converters.add(marshallingMessageConverter()); super.configureMessageConverters(converters); } @Bean public InternalResourceViewResolver setupViewResolver() { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setViewClass(JstlView.class); viewResolver.setPrefix("/WEB-INF/jsp/"); viewResolver.setSuffix(".jsp"); return viewResolver; } @Bean public JacksonAnnotationIntrospector jacksonAnnotationIntrospector() { return new JacksonAnnotationIntrospector(); } @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.setAnnotationIntrospector(jacksonAnnotationIntrospector()); mapper.registerModule(new JodaModule()); mapper.registerModule(new Hibernate4Module()); return mapper; } @Bean public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); messageConverter.setObjectMapper(objectMapper()); return messageConverter; } @Bean(name = "marshaller") public Jaxb2Marshaller jaxb2Marshaller() { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("com.x); return marshaller; } @Bean public MarshallingHttpMessageConverter marshallingMessageConverter() { return new MarshallingHttpMessageConverter( jaxb2Marshaller(), jaxb2Marshaller() ); } }

Configuración de contexto de raíz de primavera:

@Configuration @EnableTransactionManagement @ComponentScan(basePackages = {"com.x.services"}, // scan for all annotated classes for the root context OTHER than controllers -- those are in the child web context. also don''t rescan these config files excludeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Configuration.class) } ) public class ServiceConfig { @Bean public DefaultAnnotationHandlerMapping defaultAnnotationHandlerMapping() { DefaultAnnotationHandlerMapping handlerMapping = new DefaultAnnotationHandlerMapping(); handlerMapping.setAlwaysUseFullPath(true); handlerMapping.setDetectHandlersInAncestorContexts(true); return handlerMapping; } @Bean public DefaultConversionService defaultConversionService() { return new DefaultConversionService(); } @Bean(name = "kmlContext") public JAXBContext kmlContext() throws JAXBException { return JAXBContext.newInstance("net.opengis.kml"); } @Bean(name = "ogcContext") public JAXBContext ogcContext() throws JAXBException { return JAXBContext.newInstance("net.x"); } }

Seguridad de primavera:

@Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomAuthenticationProvider customAuthenticationProvider; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { AuthenticationProvider rememberMeAuthenticationProvider = rememberMeAuthenticationProvider(); TokenBasedRememberMeServices tokenBasedRememberMeServices = tokenBasedRememberMeServices(); List<AuthenticationProvider> authenticationProviders = new ArrayList<AuthenticationProvider>(2); authenticationProviders.add(rememberMeAuthenticationProvider); authenticationProviders.add(customAuthenticationProvider); AuthenticationManager authenticationManager = authenticationManager(authenticationProviders); http .csrf().disable() //.headers().disable() .headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)) .and() .authenticationProvider(customAuthenticationProvider) .addFilter(new RememberMeAuthenticationFilter(authenticationManager, tokenBasedRememberMeServices)) .rememberMe().rememberMeServices(tokenBasedRememberMeServices) .and() .authorizeRequests() .antMatchers("/js/**", "/css/**", "/img/**", "/login", "/processLogin").permitAll() .antMatchers("/index.jsp", "/index.html", "/index").hasRole("USER") .antMatchers("/admin", "/admin.html", "/admin.jsp", "/js/saic/jswe/admin/**").hasRole("ADMIN") .and() .formLogin().loginProcessingUrl("/processLogin").loginPage("/login").usernameParameter("username").passwordParameter("password").permitAll() .and() .exceptionHandling().accessDeniedPage("/login") .and() .logout().permitAll(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/img/**"); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(List<AuthenticationProvider> authenticationProviders) { return new ProviderManager(authenticationProviders); } @Bean public TokenBasedRememberMeServices tokenBasedRememberMeServices() { return new TokenBasedRememberMeServices("testKey", userDetailsService); } @Bean public AuthenticationProvider rememberMeAuthenticationProvider() { return new org.springframework.security.authentication.RememberMeAuthenticationProvider("testKey"); } protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } }

Configuración del intermediario de mensajes WebSocket:

@Configuration @EnableWebSocketMessageBroker @EnableScheduling public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { SockJsServiceRegistration registration = registry.addEndpoint("/hello").withSockJS().setClientLibraryUrl("http://localhost:8084/swtc/js/sockjs-0.3.4.min.js"); registration.setWebSocketEnabled(true); //registration.setSessionCookieNeeded(false); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.taskExecutor().corePoolSize(4).maxPoolSize(8); } @Override public void configureClientOutboundChannel(ChannelRegistration registration) { registration.taskExecutor().corePoolSize(4).maxPoolSize(8); } }

Controlador WebSocket:

@Controller public class WebSocketController { @MessageMapping({"/hello", "/hello/**"}) @SendTo("/topic/greetings") // in order to get principal, you must set cookiesNeeded in WebSocketConfig, which forces IE to use iframes, which doesn''t seem to work public AjaxResponse<String> greeting(@Payload PointRadiusRequest prr, Principal principal) throws Exception { Thread.sleep(3000); // simulated delay AjaxResponse<String> ajaxResponse = new AjaxResponse<String>(); ajaxResponse.setValue(principal.getName()); ajaxResponse.setSuccess(true); return ajaxResponse; } }

Y finalmente, el javascript en mi html que estoy usando para probar:

<script> // test/prototype websocket code stompClient = null; window.connect = function() { var options = {protocols_whitelist: ["websocket", "xhr-streaming", "xdr-streaming", "xhr-polling", "xdr-polling", "iframe-htmlfile", "iframe-eventsource", "iframe-xhr-polling"], debug: true}; wsSocket = new SockJS(''rest/hello'', undefined, options); stompClient = Stomp.over(wsSocket); stompClient.connect({}, function(frame) { console.log(''Connected: '' + frame); stompClient.subscribe(''/topic/greetings'', function(message) { console.info("response: ", JSON.parse(message.body)); }); }); }; window.disconnect = function() { stompClient.disconnect(); console.log("Disconnected"); }; window.sendName = function() { stompClient.send("/app/hello", {}, JSON.stringify({''latitude'': 12, ''longitude'': 123.2, radius: 3.14})); }; </script>

Cuando me conecto en Firefox, esto es lo que veo en la consola:

>>> connect() connecting /swtc/ (line 109) Opening Web Socket... stomp.js (line 130) undefined GET http://localhost:8084/swtc/rest/hello/info 200 OK 202ms sockjs....min.js (line 27) Opening transport: websocket url:rest/hello/007/xkc17fkt RTO:912 sockjs....min.js (line 27) SyntaxError: An invalid or illegal string was specified ...3,reason:"All transports failed",wasClean:!1,last_event:g})}f.readyState=y.CLOSE... sockjs....min.js (line 27) Closed transport: websocket SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false) sockjs....min.js (line 27) Opening transport: xhr-streaming url:rest/hello/007/8xz79yip RTO:912 sockjs....min.js (line 27) POST http://localhost:8084/swtc/rest/hello/007/8xz79yip/xhr_streaming 200 OK 353ms sockjs....min.js (line 27) Web Socket Opened... >>> CONNECT accept-version:1.1,1.0 heart-beat:10000,10000 � stomp.js (line 130) POST http://localhost:8084/swtc/rest/hello/007/8xz79yip/xhr_send 204 No Content 63ms <<< CONNECTED user-name:first.mi.last heart-beat:0,0 version:1.1 � stomp.js (line 130) connected to server undefined stomp.js (line 130) Connected: CONNECTED version:1.1 heart-beat:0,0 user-name:xxx >>> SUBSCRIBE id:sub-0 destination:/topic/greetings � stomp.js (line 130) POST http://localhost:8084/swtc/rest/hello/007/8xz79yip/xhr_send 204 No Content 57ms

La respuesta / info es:

{"entropy":441118013,"origins":["*:*"],"cookie_needed":true,"websocket":true}

Tenga en cuenta el extraño error de cadena cuando intenta hacer la conexión del websocket. Supongo que esa es la fuente de mis problemas, pero no estoy haciendo nada gracioso y no tengo idea de qué lo está causando.

En IE, aquí está el tráfico de la red. Los archivos iframe.html parecen estar construidos correctamente, pero simplemente no pueden conectarse al back-end.

URL Method Result Type Received Taken Initiator Wait‎‎ Start‎‎ Request‎‎ Response‎‎ Cache read‎‎ Gap‎‎ /swtc/rest/hello/info?t=1399328502157 GET 200 application/json 411 B 328 ms 0 47 281 0 0 2199 /swtc/rest/hello/iframe.html GET 200 text/html 0.97 KB 156 ms frame navigate 328 0 156 0 0 2043 /swtc/js/sockjs-0.3.4.min.js GET 304 application/javascript 157 B < 1 ms <script> 484 0 0 0 0 2043 /swtc/rest/hello/iframe.html GET 304 text/html 191 B < 1 ms frame navigate 2527 0 0 0 0 0 /swtc/js/sockjs-0.3.4.min.js GET 304 application/javascript 157 B < 1 ms <script> 2527 0 0 0 0 0

La respuesta de información se ve así:

{"entropy":-475136625,"origins":["*:*"],"cookie_needed":true,"websocket":true}

Si alguien quiere ver los encabezados de solicitud o respuesta, solo házmelo saber.

ACTUALIZACIÓN 1:

Rossen, gracias por la respuesta. Todo lo que sé sobre la primavera 4 lo aprendí de ti :)

Firefox no está funcionando (completamente), no puedo obtener una sesión de websocket, se degrada a xhr-streaming. Con xhr-streaming, no hay problemas, pero me gustaría tener una verdadera sesión de websocket.

Con IE, no estoy seguro de qué será lo que quite los encabezados. Pensé que el encabezado x frame solo afectaba a la sesión iframe, que no funciona en absoluto. IE utiliza xdr-streaming (y funciona, aunque sin la capacidad de recuperar el Principal) cuando desactivo requieren cookies. Una vez que habilito las cookies, IE INTENTAMENTE INTENTA usar iframes. Pero incluso con los encabezados en su lugar, todos los intentos fallan:

http://localhost:8084/swtc/rest/hello/info?t=1399328502157 Key Value Response HTTP/1.1 200 OK Server Apache-Coyote/1.1 X-Frame-Options SAMEORIGIN Access-Control-Allow-Origin http://localhost:8084 Access-Control-Allow-Credentials true Cache-Control no-store, no-cache, must-revalidate, max-age=0 Content-Type application/json;charset=UTF-8 Content-Length 78 Date Mon, 05 May 2014 22:21:42 GMT LOG: Opening Web Socket... LOG: Opening transport: iframe-htmlfile url:rest/hello/904/ft3apk1g RTO:1008 LOG: Closed transport: iframe-htmlfile SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) LOG: Opening transport: iframe-xhr-polling url:rest/hello/904/bf63eisu RTO:1008 LOG: Closed transport: iframe-xhr-polling SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) LOG: Whoops! Lost connection to undefined

Ambos iframe-htmlfile y iframe-xhr-polling fallan. De hecho, borro el caché con cada actualización en IE y tengo habilitado el modo de depuración en SockJS. Estaría bien viviendo con xdr-streaming en IE, pero realmente necesito la cookie jsessionid.

¿Alguna idea?

En una nota lateral, sería realmente bueno si el código de la biblioteca del cliente soportara rutas relativas (en realidad construye el archivo html con la ruta relativa y debería funcionar, pero aún produce errores en el registro), es decir:

SockJsServiceRegistration registration = registry.addEndpoint("/hello").withSockJS().setClientLibraryUrl("js/sockjs-0.3.4.min.js");

Eso haría que el despliegue a la producción sea menos doloroso.

ACTUALIZACIÓN 2:

Resumen rápido: no hubo cambio.

Aquí está mi intento de conectar en IE9 con .headers (). Y () en mi configuración de seguridad:

LOG: Opening Web Socket... LOG: Opening transport: iframe-htmlfile url:rest/hello/924/1ztfjm7z RTO:330 LOG: Closed transport: iframe-htmlfile SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false) LOG: Opening transport: iframe-xhr-polling url:rest/hello/924/cgq8_s5j RTO:330 LOG: Closed transport: iframe-xhr-polling SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false) LOG: Whoops! Lost connection to undefined

Los encabezados de solicitud para / info:

Key Value Request GET /swtc/rest/hello/info?t=1399404419358 HTTP/1.1 Accept */* Origin http://localhost:8084 Accept-Language en-US UA-CPU AMD64 Accept-Encoding gzip, deflate User-Agent Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Host localhost:8084 Connection Keep-Alive Cache-Control no-cache

y los encabezados de respuesta:

Key Value Response HTTP/1.1 200 OK Server Apache-Coyote/1.1 X-Content-Type-Options nosniff X-XSS-Protection 1; mode=block Cache-Control no-cache, no-store, max-age=0, must-revalidate Pragma no-cache Expires 0 X-Frame-Options DENY Access-Control-Allow-Origin http://localhost:8084 Access-Control-Allow-Credentials true Cache-Control no-store, no-cache, must-revalidate, max-age=0 Content-Type application/json;charset=UTF-8 Content-Length 78 Date Tue, 06 May 2014 19:26:59 GMT

No hubo diferencia en Firefox. Obtengo el mismo error de cadena extraño cuando intenta abrir el websocket, luego vuelve a xhr-streaming:

Opening transport: websocket url:rest/hello/849/fy_06t1v RTO:342 SyntaxError: An invalid or illegal string was specified Closed transport: websocket SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false) Opening transport: xhr-streaming url:rest/hello/849/2r0raiz8 RTO:342 http://localhost:8084/swtc/rest/hello/849/2r0raiz8/xhr_streaming Web Socket Opened... >>> CONNECT accept-version:1.1,1.0 heart-beat:10000,10000


He encontrado problemas con IE9 también hoy después de una investigación descubrí que necesitaba pasar una opción de desarrollo a la llamada para crear SockJS.

var protocols = { protocols_whitelist: ["websocket", "xhr-streaming", "xdr-streaming", "xhr-polling", "xdr-polling", "iframe-htmlfile", "iframe-eventsource", "iframe-xhr-polling"]}; var opt = {debug: false, devel: true} var socket = new SockJS(''/Application/wscomms'', protocols, opt); var stompClient = Stomp.over(socket);

Dentro del archivo sockjs-0.3.4.js (en la línea 1749) encontré que se agrega un tiempo a la URL de iFrame

if (that.ri._options.devel) { iframe_url += ''?t='' + (+new Date); }

Noté que la opción de desarrollo se establece en falsa si no se pasa como una opción.

También notará que puedo pasar una URL relativa a SockJS que está funcionando. También tengo la misma configuración de Spring Security y WebSocketConfig setClientLibraryUrl () como la muestra Rossen.

También he descubierto que puedo tener

.setSessionCookieNeeded(true);

Estoy usando Spring 4.0.1, Spring Security 3.2.0 y Sockjs 0.3.4 y Tomcat 7.0.53


Dado que funciona en FF y en IE con sessionCookieNeeded = false, supongo que el problema tiene que ver con el encabezado X-Frame-Options.

Tu configuración parece correcta. Específicamente esto para Spring Security:

.headers().addHeaderWriter( new XFrameOptionsHeaderWriter( XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)).and()

y también esto para SockJS:

setClientLibraryUrl("http://localhost:8084/swtc/js/sockjs-0.3.4.min.js");

Sugiero tratar de desactivar el encabezado solo para confirmar si es el problema, es decir:

.headers().and()

También asegúrese de que no haya un problema de almacenamiento en caché del navegador que envíe la misma respuesta. Por lo tanto, compruebe los encabezados de respuesta reales para el valor de X-Frame-Options.

Para eso, recomiendo activar el modo de depuración del cliente SockJS a través del parámetro de opciones del constructor SockJS.


Como SockJS estaba produciendo un extraño error de cadena al intentar la conexión WebSocket, luego retrocedí a xhr_streaming, decidí cargar la versión no minificada del archivo .js y depurarlo en Firebug para ver qué estaba pasando. Resulta que a SockJS no le gustan las URL relativas, lo que apesta.

Para la mayoría de mis servicios REST / AJAX, tengo / rest / * asignado a mi servlet de despachador, generalmente tengo un @RequestMapping en cada controlador, y otro @RequestMapping en cada método de controlador. Utilizando Dojo, realizo llamadas AJAX especificando la url "rest/<controller>/<method>" .

Estaba intentando lo mismo con SockJS. Solo señalé "descansar / hola". Cambié esto a la URL totalmente calificada " http://localhost:8084/swtc/rest/hello " y de repente firefox podría construir la capa de transporte de websocket muy bien. Salté a IE para una prueba rápida y, efectivamente, construyó la sesión iframe y también funcionó bien.

Un pequeño problema tonto. Odio tener que especificar URLs no relativas en ninguna parte, ya que esta base de código se comparte entre varios desarrolladores, todos los que se implementan en diferentes servidores para probarlos y se implementan en producción. Supongo que en el front-end puedo construir dinámicamente la URL usando window.doc.URL, pero será un poco más complicado conseguir que AbstractWebSocketMessageBrokerConfigurer trabaje automáticamente en las implementaciones al especificar setClientLibraryUrl.

De cualquier manera, niños, no utilicen caminos relativos con SockJS.