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.