我试图基于Spring Websocket Demo运行ActiveMQ构建一个websocket消息传递应用程序,作为带有Undertow的STOMP消息代理。应用程序在不安全的连接上运行良好。但是,我很难配置STOMP Broker Relay转发SSL连接。
正如Spring WebSocket Docs中提到的那样......
" STOMP经纪人转发"在上面的配置中是Spring MessageHandler,它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的TCP连接,将所有消息转发给它,然后通过其WebSocket会话将从代理接收的所有消息转发给客户端。从本质上讲,它充当了一个"继电器"它会向两个方向转发消息。
此外,文档陈述了我对reactor-net的依赖...
请在org.projectreactor上添加依赖项:reactor-net用于TCP连接管理。
问题是我当前的实现并未通过SSL初始化NettyTCPClient,因此ActiveMQ连接因SSLException而失败。
[r.i.n.i.n.t.NettyTcpClient:307] » CONNECTED:
[id: 0xcfef39e9, /127.0.0.1:17779 => localhost/127.0.0.1:8442]
...
[o.a.a.b.TransportConnection.Transport:245] »
Transport Connection to: tcp://127.0.0.1:17779 failed:
javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?
...
因此我尝试研究Project Reactor Docs为连接设置SSL选项,但我还没有成功。
此时我发现StompBrokerRelayMessageHandler默认NettyTCPClient初始化Reactor2TcpClient Rossens,但它似乎无法配置。
非常感谢协助。
SSCCE
app.props
spring.activemq.in-memory=true
spring.activemq.pooled=false
spring.activemq.broker-url=stomp+ssl://localhost:8442
server.port=8443
server.ssl.enabled=true
server.ssl.protocol=tls
server.ssl.key-alias=undertow
server.ssl.key-store=classpath:undertow.jks
server.ssl.key-store-password=xxx
server.ssl.trust-store=classpath:undertow_certs.jks
server.ssl.trust-store-password=xxx
WebSocketConfig
//...
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static final Logger log = LoggerFactory.getLogger(WebSocketConfig.class);
private final static String KEYSTORE = "/activemq.jks";
private final static String KEYSTORE_PASS = "xxx";
private final static String KEYSTORE_TYPE = "JKS";
private final static String TRUSTSTORE = "/activemq_certs.jks";
private final static String TRUSTSTORE_PASS = "xxx";
private static String getBindLocation() {
return "stomp+ssl://localhost:8442?transport.needClientAuth=false";
}
@Bean(initMethod = "start", destroyMethod = "stop")
public SslBrokerService activeMQBroker() throws Exception {
final SslBrokerService service = new SslBrokerService();
service.setPersistent(false);
KeyManager[] km = SecurityManager.getKeyManager();
TrustManager[] tm = SecurityManager.getTrustManager();
service.addSslConnector(getBindLocation(), km, tm, null);
final ActiveMQTopic topic = new ActiveMQTopic("jms.topic.test");
service.setDestinations(new ActiveMQDestination[]{topic});
return service;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/topic").setRelayHost("localhost").setRelayPort(8442);
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/welcome").withSockJS();
registry.addEndpoint("/test").withSockJS();
}
private static class SecurityManager {
//elided...
}
}
已解决每http://ruby-doc.org/stdlib-2.1.0/libdoc/logger/rdoc/Logger.html条建议。这是所有感兴趣的人的实施细节。
WebSocketConfig
@Configuration
public class WebSocketConfig extends DelegatingWebSocketMessageBrokerConfiguration {
...
@Bean
public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler() {
StompBrokerRelayMessageHandler handler = (StompBrokerRelayMessageHandler) super.stompBrokerRelayMessageHandler();
ConfigurationReader reader = new StompClientDispatcherConfigReader();
Environment environment = new Environment(reader).assignErrorJournal();
TcpOperations<byte[]> client = new Reactor2TcpClient<>(new StompTcpClientSpecFactory(environment,"localhost", 8443));
handler.setTcpClient(client);
return handler;
}
}
StompTCPClientSpecFactory
private static class StompTcpClientSpecFactory
implements NetStreams.TcpClientFactory<Message<byte[]>, Message<byte[]>> {
private static final Logger log = LoggerFactory.getLogger(StompTcpClientSpecFactory.class);
private final String host;
private final int port;
private final String KEYSTORE = "src/main/resources/tcpclient.jks";
private final String KEYSTORE_PASS = "xxx";
private final String KEYSTORE_TYPE = "JKS";
private final String TRUSTSTORE = "/src/main/resources/tcpclient_certs.jks";
private final String TRUSTSTORE_PASS = "xxx";
private final String TRUSTSTORE_TYPE = "JKS";
private final Environment environment;
private final SecurityManager tcpManager = new SecurityManager
.SSLBuilder(KEYSTORE, KEYSTORE_PASS)
.keyStoreType(KEYSTORE_TYPE)
.trustStore(TRUSTSTORE, TRUSTSTORE_PASS)
.trustStoreType(TRUSTSTORE_TYPE)
.build();
public StompTcpClientSpecFactory(Environment environment, String host, int port) {
this.environment = environment;
this.host = host;
this.port = port;
}
@Override
public Spec.TcpClientSpec<Message<byte[]>, Message<byte[]>> apply(
Spec.TcpClientSpec<Message<byte[]>, Message<byte[]>> tcpClientSpec) {
return tcpClientSpec
.ssl(new SslOptions()
.sslProtocol("TLS")
.keystoreFile(tcpManager.getKeyStore())
.keystorePasswd(tcpManager.getKeyStorePass())
.trustManagers(tcpManager::getTrustManager)
.trustManagerPasswd(tcpManager.getTrustStorePass()))
.codec(new Reactor2StompCodec(new StompEncoder(), new StompDecoder()))
.env(this.environment)
.dispatcher(this.environment.getCachedDispatchers("StompClient").get())
.connect(this.host, this.port);
}
}
答案 0 :(得分:6)
StompBrokerRelayMessageHandler
具有您可以设置的tcpClient属性。但是,我们似乎没有通过WebSocketMessageBrokerConfigurer
设置公开它。
您可以删除@EnableWebSocketMessageBroker
并扩展DelegatingWebSocketMessageBrokerConfiguration
。它实际上是相同的,但您现在直接从提供所有bean的配置类扩展。
这允许您覆盖stompBrokerRelayMessageHandler()
bean并直接设置其TcpClient属性。只需确保覆盖方法标有@Bean
。
答案 1 :(得分:4)
我需要使用带有Java 8的Spring Messaging 4.2.5为RabbitMQ保护STOMP代理中继,并发现该问题的后续代码已经过时。
启动我的应用程序时,我提供信任库环境属性以信任内部自签名证书颁发机构。
java -Djavax.net.ssl.trustStore=/etc/pki/java/server.jks -Djavax.net.ssl.trustStorePassword=xxxxx -jar build/libs/server.war
Per Rossen的回答,我改变了
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
到
@Configuration
public class WebSocketConfig extends DelegatingWebSocketMessageBrokerConfiguration {
然后,在WebSocketConfig
中,我提供了自己的AbstractBrokerMessageHandler
bean:
@Bean
public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler() {
AbstractBrokerMessageHandler handler = super.stompBrokerRelayMessageHandler();
if (handler instanceof StompBrokerRelayMessageHandler) {
((StompBrokerRelayMessageHandler) handler).setTcpClient(new Reactor2TcpClient<>(
new StompTcpFactory("127.0.0.1", 61614, true)
));
}
return handler;
}
有条件的实例是在单元测试中简化NoOpBrokerMessageHandler
的使用。
最后,以下是上面使用的StompTcpFactory的实现:
public class StompTcpFactory implements NetStreams.TcpClientFactory<Message<byte[]>, Message<byte[]>> {
private final Environment environment = new Environment(new SynchronousDispatcherConfigReader());
private final String host;
private final int port;
private final boolean ssl;
public StompTcpFactory(String host, int port, boolean ssl) {
this.host = host;
this.port = port;
this.ssl = ssl;
}
@Override
public Spec.TcpClientSpec<Message<byte[]>, Message<byte[]>> apply(Spec.TcpClientSpec<Message<byte[]>, Message<byte[]>> tcpClientSpec) {
return tcpClientSpec
.env(environment)
.codec(new Reactor2StompCodec(new StompEncoder(), new StompDecoder()))
.ssl(ssl ? new SslOptions() : null)
.connect(host, port);
}
private static class SynchronousDispatcherConfigReader implements ConfigurationReader {
@Override
public ReactorConfiguration read() {
return new ReactorConfiguration(Collections.emptyList(), "sync", new Properties());
}
}
}
答案 2 :(得分:3)
@amoebob 答案很棒但线程未正确关闭。每次打开来自客户端的连接时,新线程都会打开并且永不关闭。我在生产中发现了这个问题,花了几天时间来解决它。所以我建议你改变StompTcpFactory来改进线程重用:
import io.netty.channel.EventLoopGroup;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.stomp.Reactor2StompCodec;
import org.springframework.messaging.simp.stomp.StompDecoder;
import org.springframework.messaging.simp.stomp.StompEncoder;
import org.springframework.messaging.tcp.reactor.Reactor2TcpClient;
import reactor.Environment;
import reactor.core.config.ReactorConfiguration;
import reactor.io.net.NetStreams;
import reactor.io.net.Spec;
import reactor.io.net.config.SslOptions;
import reactor.io.net.impl.netty.NettyClientSocketOptions;
public class StompTcpFactory implements NetStreams.TcpClientFactory<Message<byte[]>, Message<byte[]>> {
private final Environment environment;
private final EventLoopGroup eventLoopGroup;
private final String host;
private final int port;
private final boolean ssl;
public StompTcpFactory(String host, int port, boolean ssl) {
this.host = host;
this.port = port;
this.ssl = ssl;
this.environment = new Environment(() -> new ReactorConfiguration(emptyList(), "sync", new Properties()));
this.eventLoopGroup = Reactor2TcpClient.initEventLoopGroup();
}
@Override
public Spec.TcpClientSpec<Message<byte[]>, Message<byte[]>> apply(Spec.TcpClientSpec<Message<byte[]>, Message<byte[]>> tcpClientSpec) {
return tcpClientSpec
.env(environment)
.options(new NettyClientSocketOptions().eventLoopGroup(eventLoopGroup))
.codec(new Reactor2StompCodec(new StompEncoder(), new StompDecoder()))
.ssl(ssl ? new SslOptions() : null)
.connect(host, port);
}
}
答案 3 :(得分:0)
对于寻找更新解决方案的每个人,我设法以更简洁的方式解决了这个问题。只需通过 SSL 创建和使用自己的 TCP 客户端:
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompReactorNettyCodec;
import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
private final WebsocketProperties properties;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*");
registry.addEndpoint("/ws").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
ReactorNettyTcpClient<byte[]> tcpClient = new ReactorNettyTcpClient<>(configurer -> configurer
.host(properties.getRelayHost())
.port(properties.getRelayPort())
.secure(), new StompReactorNettyCodec());
registry.enableStompBrokerRelay("/queue", "/topic")
.setAutoStartup(true)
.setSystemLogin(properties.getClientLogin())
.setSystemPasscode(properties.getClientPasscode())
.setClientLogin(properties.getClientLogin())
.setClientPasscode(properties.getClientPasscode())
.setTcpClient(tcpClient);
registry.setApplicationDestinationPrefixes("/app");
}
}
在我的情况下(略有不同),我创建了 ReactorNettyTcpClient
的两个实现作为 Bean,并根据环境我选择了一个有/没有 SSL。
依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/>
</parent>
.
.
.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-stomp</artifactId>
<version>5.16.2</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>1.0.8</version>
</dependency>
我希望目前正在尝试解决此问题的任何人都觉得它很有用。