所以我必须说,所有的 websocket 教程/示例看起来都很简单,但似乎你真的必须挖掘才能找到简单示例中遗漏的非常重要的信息。在前端使用带有 SockJS 的 Spring 4 Stomp 消息代理时,我的 web 应用程序仍然存在很多问题。
目前,如果我在没有启用 SockJS() 的情况下向 StompEndpointRegistry 添加一个端点,然后使用 dojo 的 dojox/socket 在前端声明我的套接字,Firefox 28 将打开一个 websocket 就好了。但是,我需要 IE8 和 IE9 的支持,所以我切换到 SockJS。使用 AbstractAnnotationConfigDispatcherServletInitializer,我花了很多时间来弄清楚如何确保所有过滤器和 servlet 都设置为使用异步(为此,网络上的文档非常稀少)。一旦我解决了这个问题,我现在可以让它在 Firefox 中工作,但只能使用 xhr_streaming。sessionCookieNeeded 设置为 true 时,IE9 默认尝试使用 iframe 进行连接,但是,它会失败:
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
如果我将所需的 cookie 设置为 false,IE 将使用 xdr-streaming 并且工作正常,但是,它会丢失请求中的 jsessionid cookie,进而我失去了在控制器中获取 Principal 的能力,这对我很重要。我在 Spring Security 中启用了相同的源 x 帧标头,并且我已经验证了请求中存在标头,但它没有帮助。所以我希望能够弄清楚如何 A)使用 Firefox 中的 WebSocket 传输使 Spring 和 SockJS 正确协商,以及 B)让 IE8 和 9 正确使用 iframe 传输,这样我就可以保留 cookie。
这是我的配置/代码:
网络应用配置:
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 配置:
@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()
);
}
}
Spring根上下文配置:
@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");
}
}
弹簧安全:
@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());
}
}
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);
}
}
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;
}
}
最后,我用来测试的 html 中的 javascript:
<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>
当我在 Firefox 中连接时,这是我在控制台中看到的:
>>> 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
/info 响应是:
{"entropy":441118013,"origins":["*:*"],"cookie_needed":true,"websocket":true}
当它尝试建立 websocket 连接时,请注意奇怪的字符串错误。我猜这是我问题的根源,但我没有做任何有趣的事情,我不知道是什么原因造成的。
在 IE 中,这里是网络流量。iframe.html 文件似乎已正确构建,但它无法连接到后端。
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
信息响应如下所示:
{"entropy":-475136625,"origins":["*:*"],"cookie_needed":true,"websocket":true}
如果有人想查看请求或响应标头,请告诉我。
更新 1:
罗森,感谢您的回复。我从你那里学到的关于 Spring 4 的一切 :)
Firefox 实际上(完全)没有工作,我无法获得 websocket 会话,它降级为 xhr-streaming。使用 xhr-streaming,没有问题,但我想要一个真正的 websocket 会话。
使用 IE,我不确定删除标题会确认什么?我认为 x 帧头只影响 iframe 会话,这根本不起作用。当我禁用需要 cookie 时,IE 使用 xdr-streaming(并且可以工作,尽管无法获取主体)。一旦我启用了 cookie,IE 就会正确地尝试使用 iframe。但即使标题到位,所有尝试都会失败:
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
iframe-htmlfile 和 iframe-xhr-polling 都失败了。我确实在 IE 中每次刷新都会清除缓存,并且我确实在 SockJS 中启用了调试模式。我会很好地在 IE 中使用 xdr-streaming,但我真的需要 jsessionid cookie。
有什么想法吗?
附带说明一下,如果客户端库代码支持相对路径,那就太好了(它实际上确实使用相对路径构建了 html 文件并且应该可以工作,但仍然会在日志中产生错误),即:
SockJsServiceRegistration registration = registry.addEndpoint("/hello").withSockJS().setClientLibraryUrl("js/sockjs-0.3.4.min.js");
这将使部署到生产环境不那么痛苦。
更新 2:
快速总结:没有变化。
这是我尝试在我的安全配置中使用 .headers().and() 在 IE9 中连接:
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
/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
和响应头:
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
Firefox 没有区别。当它尝试打开 websocket 时,我得到了同样奇怪的字符串错误,然后回退到 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