来看一下场景:
有一个监控系统,需要把日志实时推送到页面上显示,你可能觉得只需要一个消费订阅通道就行了;
那再升级一下,这个监控系统同时监控了1000个应用,每个应用看到的日志是不一样的,那一个通道显然不够了。
由于历史遗留问题,这里采用的ActiveMQ来做消息中间件,在之前的方案中:前端是直连MQ的,基于Stomp协议,
一切都工作得很好,直到有一天发现了MQ里的入侵代码。。。
下面来演示这种场景:
容易被攻击的ActiveMQ
前端
var destination = "该应用的队列名";
var client = Stomp.client('ws://IP/stomp');
var callbackMSG = function(message) {
if (message.body) {
alert("got message with body " + message.body)
} else {
alert("got empty message");
}
};
var connect_callback = function(frame) {
client.subscribe(destination, callbackMSG);
};
var error_callback = function(error) {
alert(error.headers.message);
};
var headers = {
login: 'xxx',
passcode: 'xxx',
// additional header
'client-id': 'my-client-id'
};
client.connect(headers, connect_callback, error_callback);
可以看到,前端需要把MQ的用户名密码写在代码里,虽然经过代码压缩,但还是很容易找到,所以是及其危险的。
PS: /stomp
是用在nginx代理的url识别,本身不需要,直连时直接用ws://MQIP:61614
。
MQ配置
<transportConnectors>
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
实际情况,61614端口经过了nginx代理:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server{
server_name localhost 域名;
listen 80;
location /stomp {
proxy_pass http://localhost:61614;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
享受被攻击的过程
可以看到,通过切换到WS协议,很容易发现MQ的密码,和每10秒连接一次的心跳。
然后,每隔一段时间MQ就挂了,接着在日志里发现了入侵代码和外网访问的IP:
2020-05-14 17:41:53,198 | INFO | /*1*/{{911191425+938578280}}_nimda Inactive for longer than 30000 ms - removing ...
2020-05-14 17:41:53,203 | INFO | <%- 979207619+908582738 %>_nimda Inactive for longer than 30000 ms - removing ...
2020-05-14 17:41:53,205 | INFO | sys.fn_sqlvarbasetostr(HashBytes('MD5' Inactive for longer than 30000 ms - removing
2020-05-14 17:41:53,230 | INFO | #set($c=800400123+845162035)${c}$c_nimda Inactive for longer than 30000 ms - removing ...
2020-05-13 21:11:38,495 | WARN | Transport Connection to: tcp://59.151.65.101:40204 failed: java.io.IOException:
Unexpected error occurred: java.lang.OutOfMemoryError: GC overhead limit exceeded | org.apache.activemq.broker.TransportConnection.Transport | ActiveMQ Transport: tcp:///59.151.65.101:40204@61616
这里只是一部分,最后因为内存回收不过来,溢出导致挂掉了。
然而这些黑客不知道通过MQ拿不到什么东西,MQ里全是没用的日志。
修复方案
当然是不能直接把MQ暴露给用户,于是想在中间加一层,然后做一些认证,这不就是把网关移到程序里了吗?这就是代理。
如果基于spring项目来实现,也是有现成的工具的,具体参考文档:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#websocket-stomp
但是,仅仅了解后端的协议还不够,前端也得跟上,这应该就是全栈的优势。
后端代理
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
增加ws协议配置
@EnableWebSocket
@SpringBootApplication
public class DemoWebsocketProxyApplication {}
import com.jimo.demo.ws.proxy.interceptor.WsHandShakeInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
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
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/**").setAllowedOrigins("*")
.withSockJS()
.setInterceptors(new WsHandShakeInterceptor());
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue", "/topic")
.setRelayHost("development01")
.setRelayPort(61613)
// 给客户端的密码
.setClientLogin("xxx")
.setClientPasscode("xxx")
.setSystemLogin("xxx")
.setSystemPasscode("xxx");
}
}
注意:
- 这里是61613端口,stomp协议,不再是websocket协议了。
- endpoint(
/stomp/**"
)代表前端访问的端点,只代理这部分开头的 - 我们设置了握手的拦截器,这是关键
拦截器:
public class WsHandShakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// TODO 在这里做拦截
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
}
}
假如后端程序的端口为8088.
前端
var sock = new SockJS('http://localhost:8088/stomp');
sock.onopen = function() {
console.log('open');
sock.send('test');
};
sock.onmessage = function(e) {
console.log('message', e.data);
sock.close();
};
sock.onclose = function() {
console.log('close');
};
var stompClient = Stomp.over(sock);
var headers = {
login: 'xxx',
passcode: 'xxx',
// additional header
'client-id': 'my-client-id'
};
stompClient.connect({}, function (frame) {
stompClient.subscribe('/queue/队列名', function (msg) {
console.log(msg);
});
});
注意:
- 这里的url是http协议,基于sockjs实现,然后再封装了一层stomp协议客户端,为了传输用户名和密码
- 假如MQ没设置用户名密码,那么也不需要用stomp代理了,sockjs就够用
- 具体的拦截认证这里就不能透露了,你懂的
总结
安全很重要,对外的应用需要不断和黑客斗智斗勇,还是做政府的项目好,完全部署在内网,啥都不用考虑。