來看一下場景:
有一個監控系統,需要把日誌實時推送到頁面上顯示,你可能覺得只需要一個消費訂閱通道就行了;
那再升級一下,這個監控系統同時監控了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就夠用
- 具體的攔截認證這裏就不能透露了,你懂的
總結
安全很重要,對外的應用需要不斷和黑客鬥智鬥勇,還是做政府的項目好,完全部署在內網,啥都不用考慮。