Real-Time Personalized Notifications with Spring Boot WebSocket and Angular [Part-3]

In Part 2 of our series, we integrated an Angular client with a Spring Boot WebSocket server to receive real-time events. In this article, we'll take things further by allowing the server to send personalized events to each connected user. By the end of this article, each user will only receive WebSocket messages relevant to them, based on their username.

What We’ll Build

We'll enhance the existing WebSocket setup to support sending messages to specific users based on their unique usernames. When a user logs in and connects to the WebSocket server, they’ll receive only messages intended for them, keeping communications private and secure.

Prerequisites

  • Part 1: Setting up a Spring Boot WebSocket Server
  • Part 2: Integrating Angular with Spring Boot WebSocket Server

Server-Side Modifications for User-Specific Notifications

Setting Up Spring Boot for User-Specific Messages

To send messages to individual users, we’ll set up a WebSocket handler that maps users to their WebSocket sessions. We’ll then use this map to push notifications only to the intended recipient.

Step 1: WebSocket Configuration

We’ll continue using WebSocketConfigurer, modifying the WebSocketConfig to register our custom handler for handling user-specific WebSocket connections.


import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final UserSpecificWebSocketHandler userSpecificWebSocketHandler;

    public WebSocketConfig(UserSpecificWebSocketHandler userSpecificWebSocketHandler) {
        this.userSpecificWebSocketHandler = userSpecificWebSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(userSpecificWebSocketHandler, "/ws").setAllowedOrigins("*");
    }
}



									

Step 2: Implementing User-Specific WebSocketHandler

We’ll implement a UserSpecificWebSocketHandler that stores each user’s session in a concurrent map, using their username as the key. This setup allows us to target specific users when sending notifications.


import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

@Component
public class UserSpecificWebSocketHandler extends TextWebSocketHandler {

    private final Map userSessions = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String username = session.getPrincipal().getName();  // Assume principal name is the username
        userSessions.put(username, session);
        System.out.println("User " + username + " connected.");
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String username = session.getPrincipal().getName();
        String payload = message.getPayload();
        
        // Handle incoming message here, e.g., log or respond
        sendMessageToUser(username, "Hello, " + username + "! Your message: " + payload);
    }

    public void sendMessageToUser(String username, String message) {
        WebSocketSession session = userSessions.get(username);
        if (session != null && session.isOpen()) {
            try {
                session.sendMessage(new TextMessage(message));
            } catch (Exception e) {
                System.out.println("Error sending message to user " + username + ": " + e.getMessage());
            }
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String username = session.getPrincipal().getName();
        userSessions.remove(username);
        System.out.println("User " + username + " disconnected.");
    }
}
	
									

Step 3: Configuring Authentication for Username-Based Connections

Since WebSocket sessions need to be authenticated, we assume users are authenticated, and their username can be accessed via session.getPrincipal().getName(). In a production application, you might configure Spring Security with JWTs or session-based authentication to achieve this.

Angular Client for User-Specific Messages

Next, let’s update our Angular client to allow users to enter a username, connect to the WebSocket server, and receive only messages sent to them.

Step 4: Modify WebSocketService to Include Username

We’ll modify WebSocketService to accept a username and connect to the WebSocket server with this information.


import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class WebSocketService {
  private ws: WebSocket | null = null;
  private eventSubject = new Subject();

  public connect(username: string) {
    const wsUrl = `ws://localhost:8080/ws?username=${username}`;

    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        console.log('WebSocket connection opened for user:', username);
      };

      this.ws.onmessage = (event) => {
        console.log('Received event:', event.data);
        this.eventSubject.next(event.data);
      };

      this.ws.onclose = () => {
        console.log('WebSocket connection closed');
      };
    }
  }

  public getEvents(): Observable {
    return this.eventSubject.asObservable();
  }
}

									

Step 5: Update Angular Component to Connect with Username

We’ll update the component to prompt the user for their username and then connect.


import { Component, OnInit } from '@angular/core';
import { WebSocketService } from './services/web-socket.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  messages: string[] = [];
  username: string = '';

  constructor(private webSocketService: WebSocketService) {}

  ngOnInit(): void {}

  connectToWebSocket() {
    if (this.username) {
      this.webSocketService.connect(this.username);
      this.webSocketService.getEvents().subscribe((message) => {
        this.messages.push(message);
      });
    } else {
      alert('Please enter a username to connect');
    }
  }
}
									
									

Step 6: Update the HTML Template

Add a UI to prompt the user for their username and display received messages.


<div>
  <h2>Personalized Notifications</h2>
  <input [(ngModel)]="username" placeholder="Enter username" />
  <button (click)="connectToWebSocket()">Connect</button>

  <ul>
    <li *ngFor="let message of messages">{{ message }}</li>
  </ul>
</div>

									
									

Testing the Application

  • Start your Spring Boot and Angular applications.
  • Open multiple browser windows and enter different usernames.
  • Observe that each user receives only their personalized messages.

Conclusion

In this tutorial, we built a real-time, personalized notification system where each WebSocket client only receives messages targeted specifically for them. In our next article, we’ll explore integrating secure authentication, such as JWTs, with this WebSocket setup to ensure even greater security. Stay tuned!