Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CristianRR94/springCommunity/llms.txt

Use this file to discover all available pages before exploring further.

Spring Community exposes a real-time chat system over WebSockets at the /ws-chat endpoint using the STOMP sub-protocol. Because WebSocket connections are long-lived and exist outside the normal HTTP request/response cycle, authentication cannot rely on JwtAuthFilter. Instead, the server registers JwtChannelInterceptor on the inbound channel, which intercepts every STOMP frame before it is processed. When a CONNECT frame arrives, the interceptor extracts the JWT from the frame’s Authorization native header, validates it, and binds the resolved username as the Spring Security Principal for the duration of the session.

WebSocket Endpoint

The STOMP endpoint is registered in WebSocketConfig with a single allowed origin:
PropertyValue
Endpoint URL/ws-chat
ProtocolSTOMP over raw WebSocket
Allowed originhttp://localhost:4200
SockJS fallbackNot enabled

STOMP Destinations

WebSocketConfig.configureMessageBroker() sets up two destination namespaces:
DirectionPrefixFull destination patternDescription
Client → Server/app/app/chat/{eventoId}Client sends a message to an event chat room
Server → Client/topic/topic/evento/{eventoId}Client subscribes to receive broadcast messages for an event

Connecting with Authentication

JwtChannelInterceptor.preSend() intercepts incoming STOMP frames. For CONNECT frames only, it reads the Authorization native header, strips the "Bearer " prefix, and calls jwtProviderService.extractUsername(token). If a non-null username is returned, a UsernamePasswordAuthenticationToken is constructed and set as the session’s user via accessor.setUser(). This principal is then available in message-handling controllers via the Principal parameter. The following JavaScript example uses the @stomp/stompjs library to connect and subscribe:
const stompClient = new StompJs.Client({
  brokerURL: 'ws://localhost:8080/ws-chat',
  connectHeaders: {
    Authorization: 'Bearer <accessToken>'
  }
});

stompClient.onConnect = () => {
  // Subscribe to event chat — replace 1 with the target event ID
  stompClient.subscribe('/topic/evento/1', (message) => {
    const msg = JSON.parse(message.body);
    console.log(msg);
  });
};

stompClient.onStompError = (frame) => {
  console.error('STOMP error', frame);
};

stompClient.activate();
The Authorization header value is read as a STOMP native header on the CONNECT frame. Pass it inside connectHeaders when constructing the StompJs.Client — this maps directly to the native header that JwtChannelInterceptor reads via accessor.getFirstNativeHeader("Authorization"). If the header is absent or the token cannot be parsed, no principal is set on the session and any @MessageMapping handler that reads Principal will receive null.

Sending Messages

Once connected, the client publishes chat messages to /app/chat/{eventoId}. The server routes these to WebsocketMessageController.getMensaje(), which is annotated with @MessageMapping("/chat/{eventoId}"). The message body must conform to MensajeDTO:
FieldTypeConstraintsDescription
textoStringNot blank, max 1000 charactersThe chat message content
nombreParticipanteStringDisplay name of the sender
participanteIdLongDatabase ID of the Participante
eventoIdLongDatabase ID of the target event
Example publish call:
stompClient.publish({
  destination: '/app/chat/1',
  body: JSON.stringify({
    texto: 'Hello, everyone!',
    nombreParticipante: 'alice',
    participanteId: 7,
    eventoId: 1
  })
});
The server also verifies through MensajeServiceImpl that the authenticated user is an enrolled participant of the target event before persisting the message. Sending from an unregistered account throws a SecurityException and the message is dropped.

Receiving Messages

After the server saves the message, WebsocketMessageController returns a HistorialMensajesDTO object. The @SendTo("/topic/evento/{eventoId}") annotation causes Spring to broadcast it to every subscriber on that topic. The broadcast payload shape:
FieldTypeDescription
idLongDatabase primary key of the saved message
textoStringMessage content
participanteIdLongDatabase ID of the Participante who sent it
nombreParticipanteStringDisplay name of the sender
fechaEnvioLocalDateTimeServer-side timestamp when the message was persisted
An example broadcast message received by the subscription callback:
{
  "id": 104,
  "texto": "Hello, everyone!",
  "participanteId": 7,
  "nombreParticipante": "alice",
  "fechaEnvio": "2024-11-15T10:45:22.731"
}
You can also retrieve the full message history for an event at any time via the REST endpoint GET /api/eventos/{eventoId}/mensajes, which returns an array of HistorialMensajesDTO objects ordered by createdAt ascending.

Build docs developers (and LLMs) love