Menggunakan Spring Boot untuk Implementasi WebSocket dengan STOMP

Diterbitkan: 2022-03-11

Protokol WebSocket adalah salah satu cara untuk membuat aplikasi Anda menangani pesan waktu nyata. Alternatif yang paling umum adalah polling panjang dan acara yang dikirim server. Masing-masing solusi ini memiliki kelebihan dan kekurangan. Pada artikel ini, saya akan menunjukkan kepada Anda bagaimana menerapkan WebSockets dengan Spring Boot Framework. Saya akan membahas penyiapan sisi server dan sisi klien, dan kami akan menggunakan protokol STOMP melalui WebSocket untuk berkomunikasi satu sama lain.

Sisi server akan dikodekan murni di Jawa. Tetapi, dalam kasus klien, saya akan menampilkan cuplikan yang ditulis dalam Java dan JavaScript (SockJS) karena, biasanya, klien WebSockets disematkan di aplikasi front-end. Contoh kode akan menunjukkan cara menyiarkan pesan ke banyak pengguna menggunakan model pub-sub serta cara mengirim pesan hanya ke satu pengguna. Di bagian selanjutnya dari artikel, saya akan membahas secara singkat mengamankan WebSocket dan bagaimana kami dapat memastikan bahwa solusi berbasis WebSocket kami akan tetap beroperasi bahkan ketika lingkungan tidak mendukung protokol WebSocket.

Harap dicatat bahwa topik mengamankan WebSockets hanya akan disinggung sebentar di sini karena ini adalah topik yang cukup kompleks untuk artikel terpisah. Karena ini, dan beberapa faktor lain yang saya sentuh di WebSocket di Produksi? bagian pada akhirnya, saya sarankan membuat modifikasi sebelum menggunakan pengaturan ini dalam produksi , baca sampai akhir untuk pengaturan siap produksi dengan langkah-langkah keamanan di tempat.

Protokol WebSocket dan STOMP

Protokol WebSocket memungkinkan Anda untuk mengimplementasikan komunikasi dua arah antar aplikasi. Penting untuk diketahui bahwa HTTP hanya digunakan untuk jabat tangan awal. Setelah itu terjadi, koneksi HTTP ditingkatkan ke koneksi TCP/IP yang baru dibuka yang digunakan oleh WebSocket.

Protokol WebSocket adalah protokol tingkat rendah. Ini mendefinisikan bagaimana aliran byte diubah menjadi bingkai. Bingkai mungkin berisi teks atau pesan biner. Karena pesan itu sendiri tidak memberikan informasi tambahan tentang cara merutekan atau memprosesnya, Sulit untuk mengimplementasikan aplikasi yang lebih kompleks tanpa menulis kode tambahan. Untungnya, spesifikasi WebSocket memungkinkan penggunaan sub-protokol yang beroperasi pada tingkat aplikasi yang lebih tinggi. Salah satunya, yang didukung oleh Spring Framework, adalah STOMP.

STOMP adalah protokol perpesanan berbasis teks sederhana yang awalnya dibuat untuk bahasa skrip seperti Ruby, Python, dan Perl untuk terhubung ke broker pesan perusahaan. Berkat STOMP, klien dan broker yang dikembangkan dalam berbagai bahasa dapat mengirim dan menerima pesan ke dan dari satu sama lain. Protokol WebSocket terkadang disebut TCP untuk Web. Secara analog, STOMP disebut HTTP untuk Web. Ini mendefinisikan beberapa jenis bingkai yang dipetakan ke bingkai WebSockets, misalnya CONNECT , SUBSCRIBE , UNSUBSCRIBE , ACK , atau SEND . Di satu sisi, perintah ini sangat berguna untuk mengelola komunikasi sementara, di sisi lain, perintah ini memungkinkan kita untuk mengimplementasikan solusi dengan fitur yang lebih canggih seperti pengakuan pesan.

Sisi Server: Spring Boot dan WebSockets

Untuk membangun sisi server WebSocket, kami akan menggunakan kerangka kerja Spring Boot yang secara signifikan mempercepat pengembangan aplikasi mandiri dan web di Java. Spring Boot menyertakan modul spring-WebSocket , yang kompatibel dengan standar Java WebSocket API (JSR-356).

Menerapkan sisi server WebSocket dengan Spring Boot bukanlah tugas yang sangat rumit dan hanya mencakup beberapa langkah, yang akan kita lalui satu per satu.

Langkah 1. Pertama, kita perlu menambahkan ketergantungan perpustakaan WebSocket.

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>

Jika Anda berencana menggunakan format JSON untuk pesan yang dikirimkan, Anda mungkin ingin menyertakan juga ketergantungan GSON atau Jackson. Kemungkinan besar, Anda juga memerlukan kerangka kerja keamanan, misalnya, Spring Security.

Langkah 2. Kemudian, kita dapat mengonfigurasi Spring untuk mengaktifkan pesan WebSocket dan STOMP.

 Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mywebsockets") .setAllowedOrigins("mydomain.com").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config){ config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } }

Metode configureMessageBroker melakukan dua hal:

  1. Membuat perantara pesan dalam memori dengan satu atau lebih tujuan untuk mengirim dan menerima pesan. Pada contoh di atas, ada dua prefiks tujuan yang ditentukan: topic dan queue . Mereka mengikuti konvensi bahwa tujuan pesan yang akan dibawa ke semua klien yang berlangganan melalui model pub-sub harus diawali dengan topic . Di sisi lain, tujuan untuk pesan pribadi biasanya diawali dengan queue .
  2. Mendefinisikan app awalan yang digunakan untuk memfilter tujuan yang ditangani oleh metode yang dianotasi dengan @MessageMapping yang akan Anda terapkan di pengontrol. Pengontrol, setelah memproses pesan, akan mengirimkannya ke broker.

Spring Boot WebSocket: Bagaimana pesan ditangani di sisi server

Bagaimana pesan ditangani di sisi server (sumber: Dokumentasi pegas)


Kembali ke cuplikan di atas—mungkin Anda telah melihat panggilan ke metode withSockJS() —itu mengaktifkan opsi fallback SockJS. Singkatnya, ini akan membuat WebSocket kami berfungsi meskipun protokol WebSocket tidak didukung oleh browser internet. Saya akan membahas topik ini secara lebih rinci sedikit lebih jauh.

Ada satu hal lagi yang perlu diklarifikasi—mengapa kita memanggil metode setAllowedOrigins() di titik akhir. Ini sering diperlukan karena perilaku default WebSocket dan SockJS adalah hanya menerima permintaan dengan asal yang sama. Jadi, jika klien Anda dan sisi server menggunakan domain yang berbeda, metode ini perlu dipanggil untuk memungkinkan komunikasi di antara mereka.

Langkah 3 . Menerapkan pengontrol yang akan menangani permintaan pengguna. Ini akan menyiarkan pesan yang diterima ke semua pengguna yang berlangganan topik tertentu.

Berikut adalah contoh metode yang mengirim pesan ke tujuan /topic/news .

 @MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }

Alih-alih anotasi @SendTo , Anda juga dapat menggunakan SimpMessagingTemplate yang dapat Anda autowire di dalam pengontrol Anda.

 @MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }

Pada langkah selanjutnya, Anda mungkin ingin menambahkan beberapa kelas tambahan untuk mengamankan titik akhir Anda, seperti ResourceServerConfigurerAdapter atau WebSecurityConfigurerAdapter dari kerangka kerja Spring Security. Juga, seringkali bermanfaat untuk mengimplementasikan model pesan sehingga JSON yang ditransmisikan dapat dipetakan ke objek.

Membangun Klien WebSocket

Menerapkan klien adalah tugas yang lebih sederhana.

Langkah 1. Klien STOMP Autowire Spring.

 @Autowired private WebSocketStompClient stompClient;

Langkah 2. Buka koneksi.

 StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();

Setelah ini dilakukan, adalah mungkin untuk mengirim pesan ke tujuan. Pesan akan dikirim ke semua pengguna yang berlangganan suatu topik.

 stompSession.send("topic/greetings", "Hello new user");

Dimungkinkan juga untuk berlangganan pesan.

 session.subscribe("topic/greetings", this); @Override public void handleFrame(StompHeaders headers, Object payload) { Message msg = (Message) payload; logger.info("Received : " + msg.getText()+ " from : " + msg.getFrom()); }

Terkadang diperlukan untuk mengirim pesan hanya ke pengguna khusus (misalnya saat menerapkan obrolan). Kemudian, klien dan sisi server harus menggunakan tujuan terpisah yang didedikasikan untuk percakapan pribadi ini. Nama tujuan dapat dibuat dengan menambahkan pengenal unik ke nama tujuan umum, misalnya /queue/chat-user123 . Sesi HTTP atau pengidentifikasi sesi STOMP dapat digunakan untuk tujuan ini.

Musim semi membuat pengiriman pesan pribadi jauh lebih mudah. Kita hanya perlu membubuhi keterangan metode Controller dengan @SendToUser . Kemudian, tujuan ini akan ditangani oleh UserDestinationMessageHandler , yang bergantung pada pengenal sesi. Di sisi klien, saat klien berlangganan ke tujuan yang diawali dengan /user , tujuan ini diubah menjadi tujuan unik untuk pengguna ini. Di sisi server, tujuan pengguna diselesaikan berdasarkan Principal pengguna.

Contoh kode sisi server dengan anotasi @SendToUser :

 @MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }

Atau Anda dapat menggunakan SimpMessagingTemplate :

 String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);

Sekarang mari kita lihat bagaimana mengimplementasikan klien JavaScript (SockJS) yang mampu menerima pesan pribadi yang dapat dikirim oleh kode Java pada contoh di atas. Perlu diketahui bahwa WebSockets adalah bagian dari spesifikasi HTML5 dan didukung oleh sebagian besar browser modern (Internet Explorer mendukungnya sejak versi 10).

 function connect() { var socket = new SockJS('/greetings'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { stompClient.subscribe('/user/queue/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).name); }); }); } function sendName() { stompClient.send("/app/greetings", {}, $("#name").val()); }

Seperti yang mungkin telah Anda catat, untuk menerima pesan pribadi, klien perlu berlangganan ke tujuan umum /queue/greetings yang diawali dengan /user . Itu tidak perlu repot dengan pengidentifikasi unik apa pun. Namun, klien perlu login ke aplikasi sebelumnya, sehingga objek Principal di sisi server diinisialisasi.

Mengamankan WebSocket

Banyak aplikasi web menggunakan otentikasi berbasis cookie. Misalnya, kita dapat menggunakan Spring Security untuk membatasi akses ke halaman tertentu atau Controller hanya untuk pengguna yang login. Konteks keamanan pengguna kemudian dipertahankan melalui sesi HTTP berbasis cookie yang kemudian dikaitkan dengan sesi WebSocket atau SockJS yang dibuat untuk pengguna tersebut. Titik akhir WebSockets dapat diamankan seperti permintaan lainnya, misalnya, di WebSecurityConfigurerAdapter Spring.

Saat ini, aplikasi web sering menggunakan REST API sebagai back-end dan token OAuth/JWT untuk otentikasi dan otorisasi pengguna. Protokol WebSocket tidak menjelaskan bagaimana server harus mengotentikasi klien selama jabat tangan HTTP. Dalam praktiknya, header HTTP standar (misalnya, Otorisasi) digunakan untuk tujuan ini. Sayangnya, ini tidak didukung oleh semua klien STOMP. Klien STOMP Spring Java memungkinkan untuk mengatur header untuk jabat tangan:

 WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);

Tetapi klien JavaScript SockJS tidak mendukung pengiriman header otorisasi dengan permintaan SockJS. Namun, ini memungkinkan untuk mengirim parameter kueri yang dapat digunakan untuk meneruskan token. Pendekatan ini memerlukan penulisan kode khusus di sisi server yang akan membaca token dari parameter kueri dan memvalidasinya. Penting juga untuk memastikan bahwa token tidak dicatat bersama dengan permintaan (atau log dilindungi dengan baik) karena ini dapat menyebabkan pelanggaran keamanan yang serius.

Opsi Penggantian SockJS

Integrasi dengan WebSocket mungkin tidak selalu berjalan mulus. Beberapa browser (misalnya, IE 9) tidak mendukung WebSockets. Terlebih lagi, proxy yang membatasi mungkin membuat tidak mungkin untuk melakukan upgrade HTTP atau mereka memutuskan koneksi yang terbuka terlalu lama. Dalam kasus seperti itu, SockJS datang untuk menyelamatkan.

Transport SockJS terbagi dalam tiga kategori umum: WebSockets, HTTP Streaming, dan HTTP Long Polling. Komunikasi dimulai dengan SockJS mengirimkan GET /info untuk mendapatkan informasi dasar dari server. Berdasarkan respons tersebut, SockJS memutuskan transportasi yang akan digunakan. Pilihan pertama adalah WebSockets. Jika tidak didukung, maka, jika memungkinkan, Streaming digunakan. Jika opsi ini juga tidak memungkinkan, maka Polling dipilih sebagai metode transportasi.

WebSocket dalam Produksi?

Meskipun pengaturan ini berfungsi, ini bukan yang "terbaik". Spring Boot memungkinkan Anda untuk menggunakan sistem pesan lengkap apa pun dengan protokol STOMP (misalnya, ActiveMQ, RabbitMQ), dan broker eksternal dapat mendukung lebih banyak operasi STOMP (misalnya, pengakuan, penerimaan) daripada broker sederhana yang kami gunakan. STOMP Over WebSocket menyediakan informasi menarik tentang WebSocket dan protokol STOMP. Ini mencantumkan sistem pesan yang menangani protokol STOMP dan bisa menjadi solusi yang lebih baik untuk digunakan dalam produksi. Apalagi jika karena banyaknya permintaan, broker pesan perlu di-cluster. (Broker pesan sederhana Spring tidak cocok untuk pengelompokan.) Kemudian, alih-alih mengaktifkan pialang sederhana di WebSocketConfig , diperlukan untuk mengaktifkan relai pialang Stomp yang meneruskan pesan ke dan dari pialang pesan eksternal. Singkatnya, broker pesan eksternal dapat membantu Anda membangun solusi yang lebih terukur dan kuat.

Jika Anda siap untuk melanjutkan perjalanan Pengembang Java menjelajahi Spring Boot, coba baca Menggunakan Spring Boot untuk OAuth2 dan JWT REST Protection selanjutnya.