前段时间公司的项目要给websocket连接加ssl和digest认证,我们用的是spring websocket的实现。网上介绍了两种给websocket加ssl的方法,一种是websocketClient.setWebsocketFactory(websocketFactory),另一种是websocketClient.getUserProperties().put("org.apache.tomcat.websocket.SSL_CONTEXT", sslContext)。但是这两种方法是针对纯websocket的,而不是spring websocket。前一种方法在spring websocket中没有对应的实现,后一种方法我试过但是没有奏效。后来通过阅读源码发现一个简单有效的办法,在这里分享大家以供参考。
SSL
websocket协议是从http upgrade上来的,websocket的安全和认证也是基于http。给websocket加ssl,实际是把http转变成https,只要让tomcat运行在8443端口(我配的是8443)即可。server的代码如下:
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableAutoConfiguration
@EnableScheduling
@ComponentScan
@SpringBootApplication
public class WssBrokerApplication {
public static void main(String[] args) {
SpringApplication.run(WssBrokerApplication.class, args);
}
@Profile("ssl")
@Bean
EmbeddedServletContainerCustomizer containerCustomizer(@Value("${keystore.file}") Resource keystoreFile,
@Value("${keystore.pass}") String keystorePass) throws Exception {
String absoluteKeystoreFile = keystoreFile.getFile().getAbsolutePath();
return (ConfigurableEmbeddedServletContainer container) -> {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container;
tomcat.addConnectorCustomizers((connector) -> {
connector.setPort(8443);
connector.setSecure(true);
connector.setScheme("https");
Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler();
proto.setSSLEnabled(true);
proto.setKeystoreFile(absoluteKeystoreFile);
proto.setKeystorePass(keystorePass);
proto.setKeystoreType("PKCS12");
proto.setKeyAlias("tomcat");
}
);
}
};
}
}
配置文件application.porperties放在resources目录下:
spring.profiles.active: ssl
keystore.file: demo.keystore
keystore.pass: changeit
Digest
给websocket配置digest认证:
spring security的配置参照上一篇博文digest认证:http://blog.csdn.net/xiaoyaoyulinger/article/details/60881279,然后给websocket加digest支持。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WssBrokerConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated()
.simpSubscribeDestMatchers("/topic/notification").permitAll()
.simpDestMatchers("/**").authenticated()
.anyMessage().denyAll();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/ws");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/hpdm-ws").setAllowedOrigins("*").withSockJS();
}
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter(mapper);
return converter;
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
Client
server配置完后,websocket client也要支持ssl和digest。在rest template中配置ssl和digest信息,然后把rest template加到sockJs client中。下面是client的代码:
1. 给rest template配置ssl和digest信息:
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import com.hpi.hpdm.rest.digest.HttpComponentsClientHttpRequestFactoryDigestAuth;
/**
* SSL and digest config for rest template
*/
@Configuration
public class SSLAndDigestConfig {
@Bean
public RestOperations restOperations(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
return new RestTemplate(clientHttpRequestFactory);
}
@Bean
public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
return new HttpComponentsClientHttpRequestFactoryDigestAuth(httpClient);
}
@Bean
public HttpClient httpClient(@Value("${keystore.file}") String file,
@Value("${keystore.pass}") String password) throws Exception {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
Resource resource = new ClassPathResource(file);
trustStore.load(resource.getInputStream(), password.toCharArray());
SSLContext sslcontext =
SSLContexts.custom().loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()).build();
@SuppressWarnings("deprecation")
SSLConnectionSocketFactory sslsf =
new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1.2"}, null,
null);
return HttpClients.custom().setDefaultCredentialsProvider(provider()).setSSLSocketFactory(sslsf).useSystemProperties().build();
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
private CredentialsProvider provider() {
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("admin", "admin");
provider.setCredentials(AuthScope.ANY, credentials);
return provider;
}
}
import java.net.URI;
import org.apache.http.HttpHost;
import org.apache.http.client.AuthCache;
import org.apache.http.client.HttpClient;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
public class HttpComponentsClientHttpRequestFactoryDigestAuth extends HttpComponentsClientHttpRequestFactory {
public HttpComponentsClientHttpRequestFactoryDigestAuth(HttpClient client) {
super(client);
}
@Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
return createHttpContext(uri);
}
private HttpContext createHttpContext(URI uri) {
// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate DIGEST scheme object, initialize it and add it to the local auth cache
DigestScheme digestAuth = new DigestScheme();
// If we already know the realm name
digestAuth.overrideParamter("realm", "myrealm");
HttpHost targetHost = new HttpHost(uri.getHost(), uri.getPort());
authCache.put(targetHost, digestAuth);
// Add AuthCache to the execution context
BasicHttpContext localcontext = new BasicHttpContext();
localcontext.setAttribute(ClientContext.AUTH_CACHE, authCache);
return localcontext;
}
}
client这边也要有配置文件application.properties,指定keystore和password,配置同上。
2. 把rest template加到sockJs client中:
@Autowired
private RestOperations rest;
SockJsClient sockJsClient;
WebSocketStompClient stompClient;
List<Transport> transports = new ArrayList<>();
final WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
StandardWebSocketClient websocketClient = new StandardWebSocketClient();
transports.add(new RestTemplateXhrTransport(rest));
transports.add(new WebSocketTransport(websocketClient));
sockJsClient = new SockJsClient(transports);
stompClient = new WebSocketStompClient(sockJsClient);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
注意,要先加RestTemplateXhrTransport,然后加WebsocketTransport,因为sockJs client会按如下规则去构造URL:http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}。
详情参考http://stackoverflow.com/questions/30413380/websocketstompclient-wont-connect-to-sockjs-endpoint