Issue with SimpMessagingTemplate not sending socket message to client - java
I have a web application that uses Kafka and Spring WebSocket for emitting different events to a mobile app. Events are triggered based on the logic where mobile app is online or offline.
The logic works perfectly fine if an event is triggered when mobile app is already online. But if it's offline, the triggered event is stored in a postgres table and marked with status PENDING. So when the mobile app comes back online, these table searches for the message and then triggers the event.
In order to update the mobile status, I am using Socket Sessions, and ChannelInterceptor which updates the mobile app status on SUBSCRIBE and DISCONNECT event.
I have logs at every possible place, and all the logs are executed when the mobile app comes back online. There's no error anywhere. The last log is right before :
messagingTemplate.convertAndSendToUser();
Logs :
Device Status in request : true
Send Mode : IMMEDIATE
inside immediate never expire >>>>>
Enter: SocketService.send() with argument[s] = [{Status=200, Response={PayloadId=1415, PayloadExpiryDateTime=0, PayloadPriority=1, EventType=NEW_MESSAGE, TemporaryMessage={Messages={Data=[{Title=Message1, DataToSend={Message=Message1, Data=bZijp8pcSKwnbRhnm2khEUkaonv3XpZ6PnS0IRP+ab4p1ub2Cmuuylga6z8RASwPWbTbxbZH5ickk4HcWKeEM1Qq8PHnmAEj/VmnLmhP9UbITDrFbzWaeUcAhdWuFzD1QKfp4lmjsFl6LeswN6x+tgFi+mimURK9EaKuKiQKnP3sZHfw6Bk2o/jH4ik8hoKetO2GRxNOKs/8H9NYUS4hLP/RP3vJttUtHo3fiJd3WwWcxT+w+YZneDFmc28ECjpoJ0ZzgV7NRJlhKBcOMl7V6eV2Rax7yNC2k7zyHH/ylkhkq74heWAtde/S43S6WLTMbtc2+t1MgHNOevsxEyI/ghBE1wJzgBIdvlhFX4M1jam+AYHyXhF7CNC/Hr/i+dpw3/SogNQwIidW8FZ5EWwBJP2nYv0eZ0owK/8zwEs3FZWF9fRUeUGn7VwFNFDsf4P1FUbeWcwHA847iHBuUF2qDVtLNgQtyejGKxhn3+a8yoKyU96cpteOmGmbZPuyU3xRk401gkk4a0noS3SOjWO8B0OKyq782K3gRUoVssPV8Hg=, ReqNr=9.0000001E7}}]}, Playlists={Data=[{Title=TemporaryPlaylist, DataToSend={Message=Temporary Playlist, Data=G7Prf2ZRUh+MSBNTjLH2BT7nNB1o8zXgz/mn9leZ7Q4UoZH1MzMrJj3ISvdkQZNAbVCnYQjh5XhOnUr1kGrZpWJ/lHUyJi1coc7fdMxe0fkgBJk9SySvaFN/TvFQaUlv2wRWIoPVXmLVhukFgzLsDeVWmuzNaWZXTQCemIzrK6SaCJ+FfFAzgxG2Z7QxWkVx, ReqNr=9.0000002E7}}]}, ShowCommands={Data=[{DataToSend={Type=ShowPlaylistRequest, ShowAll=[{CallWord=t2212161322}]}}]}}}, Message=Success}, message, 108]
Sending to Producer : {"Status":200,"Response":{"PayloadId":1415,"PayloadExpiryDateTime":0,"PayloadPriority":1,"EventType":"NEW_MESSAGE","TemporaryMessage":{"Messages":{"Data":[{"Title":"Message1","DataToSend":{"Message":"Message1","Data":"bZijp8pcSKwnbRhnm2khEUkaonv3XpZ6PnS0IRP+ab4p1ub2Cmuuylga6z8RASwPWbTbxbZH5ickk4HcWKeEM1Qq8PHnmAEj/VmnLmhP9UbITDrFbzWaeUcAhdWuFzD1QKfp4lmjsFl6LeswN6x+tgFi+mimURK9EaKuKiQKnP3sZHfw6Bk2o/jH4ik8hoKetO2GRxNOKs/8H9NYUS4hLP/RP3vJttUtHo3fiJd3WwWcxT+w+YZneDFmc28ECjpoJ0ZzgV7NRJlhKBcOMl7V6eV2Rax7yNC2k7zyHH/ylkhkq74heWAtde/S43S6WLTMbtc2+t1MgHNOevsxEyI/ghBE1wJzgBIdvlhFX4M1jam+AYHyXhF7CNC/Hr/i+dpw3/SogNQwIidW8FZ5EWwBJP2nYv0eZ0owK/8zwEs3FZWF9fRUeUGn7VwFNFDsf4P1FUbeWcwHA847iHBuUF2qDVtLNgQtyejGKxhn3+a8yoKyU96cpteOmGmbZPuyU3xRk401gkk4a0noS3SOjWO8B0OKyq782K3gRUoVssPV8Hg=","ReqNr":9.0000001E7}}]},"Playlists":{"Data":[{"Title":"TemporaryPlaylist","DataToSend":{"Message":"Temporary Playlist","Data":"G7Prf2ZRUh+MSBNTjLH2BT7nNB1o8zXgz/mn9leZ7Q4UoZH1MzMrJj3ISvdkQZNAbVCnYQjh5XhOnUr1kGrZpWJ/lHUyJi1coc7fdMxe0fkgBJk9SySvaFN/TvFQaUlv2wRWIoPVXmLVhukFgzLsDeVWmuzNaWZXTQCemIzrK6SaCJ+FfFAzgxG2Z7QxWkVx","ReqNr":9.0000002E7}}]},"ShowCommands":{"Data":[{"DataToSend":{"Type":"ShowPlaylistRequest","ShowAll":[{"CallWord":"t2212161322"}]}}]}}},"Message":"Success"}
Exit: SocketService.send() with result = null
Received from Kafka : {"Status":200,"Response":{"PayloadId":1415,"PayloadExpiryDateTime":0,"PayloadPriority":1,"EventType":"NEW_MESSAGE","TemporaryMessage":{"Messages":{"Data":[{"Title":"Message1","DataToSend":{"Message":"Message1","Data":"bZijp8pcSKwnbRhnm2khEUkaonv3XpZ6PnS0IRP+ab4p1ub2Cmuuylga6z8RASwPWbTbxbZH5ickk4HcWKeEM1Qq8PHnmAEj/VmnLmhP9UbITDrFbzWaeUcAhdWuFzD1QKfp4lmjsFl6LeswN6x+tgFi+mimURK9EaKuKiQKnP3sZHfw6Bk2o/jH4ik8hoKetO2GRxNOKs/8H9NYUS4hLP/RP3vJttUtHo3fiJd3WwWcxT+w+YZneDFmc28ECjpoJ0ZzgV7NRJlhKBcOMl7V6eV2Rax7yNC2k7zyHH/ylkhkq74heWAtde/S43S6WLTMbtc2+t1MgHNOevsxEyI/ghBE1wJzgBIdvlhFX4M1jam+AYHyXhF7CNC/Hr/i+dpw3/SogNQwIidW8FZ5EWwBJP2nYv0eZ0owK/8zwEs3FZWF9fRUeUGn7VwFNFDsf4P1FUbeWcwHA847iHBuUF2qDVtLNgQtyejGKxhn3+a8yoKyU96cpteOmGmbZPuyU3xRk401gkk4a0noS3SOjWO8B0OKyq782K3gRUoVssPV8Hg=","ReqNr":9.0000001E7}}]},"Playlists":{"Data":[{"Title":"TemporaryPlaylist","DataToSend":{"Message":"Temporary Playlist","Data":"G7Prf2ZRUh+MSBNTjLH2BT7nNB1o8zXgz/mn9leZ7Q4UoZH1MzMrJj3ISvdkQZNAbVCnYQjh5XhOnUr1kGrZpWJ/lHUyJi1coc7fdMxe0fkgBJk9SySvaFN/TvFQaUlv2wRWIoPVXmLVhukFgzLsDeVWmuzNaWZXTQCemIzrK6SaCJ+FfFAzgxG2Z7QxWkVx","ReqNr":9.0000002E7}}]},"ShowCommands":{"Data":[{"DataToSend":{"Type":"ShowPlaylistRequest","ShowAll":[{"CallWord":"t2212161322"}]}}]}}},"Message":"Success"}
Sending to Socket Client.......
But the front-end never gets the message.
Below are some code snippets:
Channel Interceptor:
public class SocketInterceptor implements ChannelInterceptor {
#Autowired
SocketSessionService socketSessionService;
private final Logger log = LoggerFactory.getLogger(SocketInterceptor.class);
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
MessageHeaders headers = message.getHeaders();
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
MultiValueMap<String, String> multiValueMap = headers.get(StompHeaderAccessor.NATIVE_HEADERS,MultiValueMap.class);
if (multiValueMap !=null) {
if (multiValueMap.containsKey("destination")) {
String destination = multiValueMap.get("destination").get(0);
log.info("Destination in interceptor : " + destination);
}
}
if (accessor.getCommand()!=null) {
log.info("Client Method : " + accessor.getCommand().name());
if (accessor.getCommand().equals(StompCommand.CONNECT) || accessor.getCommand().equals(StompCommand.SUBSCRIBE) || accessor.getCommand().equals(StompCommand.SEND)) {
if (!multiValueMap.containsKey("access-token")) {
log.error("Token Error " + "No Token was provided in + " + accessor.getCommand().name());
throw new MessagingException("Forbidden: No Token was provided in header for Socket Command: " + accessor.getCommand().name());
}
String token = multiValueMap.get("access-token").get(0);
if (JwtTokenUtil.verifyToken(JwtTokenUtil.decrypt(token))) {
log.info("Token Verified Successfully >>>>>>>>>>>>>>>>>>>> ");
Claims claims = JwtTokenUtil.verifyJwtToken(JwtTokenUtil.decrypt(token));
accessor.setHeader("access-token", token);
Principal principal = new UsernamePasswordAuthenticationToken(claims.getId(), null, null);
accessor.setUser(principal);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
claims.getId(), null, null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
if (accessor.getCommand().equals(StompCommand.SUBSCRIBE)){
log.info("Storing Socket Session on Socket Command SUBSCRIBE ");
SocketSessionModel model=new SocketSessionModel();
model.setSessionId(accessor.getSessionId());
model.setDeviceId(multiValueMap.get("destination").get(0));
model.setAccessToken(token);
model.setCurrentStatus(SocketSessionStatus.ONLINE);
model.setReportedOnlineAt(ZonedDateTime.now().with(ZoneOffset.UTC));
model.setReportedOfflineAt(null);
socketSessionService.onSubscribe(model);
}
ObjectMapper objectMapper= new ObjectMapper();
try {
log.info("Returning back the frame : " + objectMapper.writeValueAsString(message.getPayload()));
} catch (Exception e){
e.printStackTrace();
}
return new GenericMessage<>(message.getPayload(), accessor.getMessageHeaders());
}
throw new MessagingException("Token either Expired or Invalid");
}
}
return message;
}
}
WebSocket Config :
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
Logger log= LoggerFactory.getLogger(WebSocketConfig.class);
#Autowired
SocketSessionService socketSessionService;
#Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/user")
.setTaskScheduler(customTaskScheduler())
.setHeartbeatValue(new long[]{25000,10000});
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(socketInterceptor());
}
#Bean
public SocketInterceptor socketInterceptor() {
return new SocketInterceptor();
}
#Bean
public TaskScheduler customTaskScheduler(){
return new ThreadPoolTaskScheduler();
}
#EventListener(SessionDisconnectEvent.class)
public void handleWsDisconnectListener(SessionDisconnectEvent event) {
Optional<String> sessionId= getSessionIdFromEvent(event);
sessionId.ifPresent(s -> socketSessionService.onDisconnect(s));
}
private Optional<String> getSessionIdFromEvent(AbstractSubProtocolEvent event) {
String sessionId = null;
System.out.println("Session Id in event : " + (event.getMessage().getHeaders()).get("simpSessionId"));
Object sessionIdAsObject = (event.getMessage().getHeaders()).get("simpSessionId");
if (nonNull(sessionIdAsObject) && sessionIdAsObject.getClass().equals(String.class)) {
sessionId = (String) sessionIdAsObject;
}
return Optional.ofNullable(StringUtils.isEmpty(sessionId) ? null : sessionId);
}
}
Kafka Producer Config :
#Service
public class KafkaProducerConfig {
#Autowired
private KafkaTemplate<String, String> kafkaTemplate;
#Async
public void send(String data, String kafkaTopic, String key) {
kafkaTemplate.send(kafkaTopic, key, data);
}
#Async
public void send(String topic, String data ){
kafkaTemplate.send(topic, data);
}
}
KafkaConsumer Config :
#EnableKafka
#Configuration
public class KafkaConsumerConfig {
#Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
public ConsumerFactory<String, String> consumerFactory(String groupId) {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(props);
}
#Bean
public ConcurrentKafkaListenerContainerFactory<String, String> rawKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory("something"));
return factory;
}
}
Kafka Listener :
This is the last step where socket emits the messages to clients. The last line of log mentioned here is executed.
#Component
public class KafkaListenerForSocket {
private final Logger log= LoggerFactory.getLogger(KafkaListenerForSocket.class);
#Autowired
SimpMessagingTemplate messagingTemplate;
#KafkaListener(topics = KafkaTopicConstants.MESSAGE, containerFactory = "rawKafkaListenerContainerFactory")
public void listenToMessages(ConsumerRecord<String, String> consumerRecord) throws IOException {
ObjectMapper objectMapper=new ObjectMapper();
String destination="/queue/message";
BaseSocketResponse response=objectMapper.readValue(consumerRecord.value(), BaseSocketResponse.class);
messagingTemplate.convertAndSendToUser(consumerRecord.key(),destination,response);
}
public void send(Object data, String key){
log.info("Bypassing Kafka and sending directly");
String destination="/queue/message";
log.info("sending to destination : " + "/user/"+key+destination);
messagingTemplate.convertAndSendToUser(key, destination, data);
}
}
BaseSocketResponse.Java class :
public class BaseSocketResponse<T> {
#JsonProperty(value = "Status", required = true)
private Integer status;
#JsonProperty(value = "Message", required = true)
private String statusMessage = "";
#JsonProperty(value = "Response", required = true)
private T response;
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getStatusMessage() {
return statusMessage;
}
public void setStatusMessage(String statusMessage) {
this.statusMessage = statusMessage;
}
public T getResponse() {
return response;
}
public void setResponse(T response) {
this.response = response;
}
}
front-end code :
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#userinfo").html("");
}
function connect() {
let header={"access-token" : $("#token").val()};
var socket = new SockJS('/services/toplight/ws');
stompClient = Stomp.over(socket);
stompClient.connect(header, function (frame) {
if(frame.command == "CONNECTED") {
console.log("Inside frame command : >>>>> " + frame);
setConnected(true);
subscribe(stompClient)
}
}, (error) => {
console.log("Inside Connect error : " + error );
onConnectError(error);
});
}
function subscribe(stompClient){
var token=$("#token").val();
var header={"access-token": token};
console.log("Subscribing to Destination : " + '/user/'+$("#name").val()+'/queue/message');
stompClient.subscribe('/user/'+$("#name").val()+'/queue/message' , greeting, header);
var greeting=function(message){
var body=JSON.parse(message.body);
if(body.Response.EventType=="NEW_MESSAGE"){
showNewMessage(response);
}
if(body.Response.EventType=="DEVICE_UPDATE_RESPONSE"){
showDeviceUpdate(response);
}
if(body.Response.EventType=="MESSAGE_UPDATE_RESPONSE"){
showMessageUpdate(response);
}
};
}
function onConnectError(error){
console.log("Error : >>>>> " + error);
$("#greetings").append("<h3><tr><td>" + error + "</h3>");
}
Related
How can I add a Principal in configureClientInboundChannel?
I am using SockJS + STOMP + Spring WebSocket with a configuration that receives user credentials during the handshake. The credentials are received in the configureClientInboundChannel method: #Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new ChannelInterceptor() { #Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (accessor != null && (StompCommand.CONNECT.equals(accessor.getCommand()) || StompCommand.SEND.equals(accessor.getCommand()))) { List<String> auth = accessor.getNativeHeader("Authorization"); System.out.printf("Authorization: %s%n", auth.get(0)); } return message; } }); } I would like to add a Principal based on the token I receive in the header. I have a handler that registers Principals: public class PrincipalHandshakeHandler extends DefaultHandshakeHandler { #Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler handler, Map<String, Object> attributes) { System.out.println(attributes); return new Principal() { #Override public String getName() { return userId; } }; } } But how do I get pass the token I get in configureClientInboundChannel to the above handshake handler? Basically, the token should go there in place of userId.
How to retrieve the current logged in user in a websocket controller
I am trying to obtain the currently authenticated user in the controller for websockets. The problem is, I cannot access the user using SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId(). I have tried to give Principal as a parameter to the method but it returns principal null. Security configuration: #Configuration #EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { #Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/connect").setAllowedOrigins("*"); } #Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic/messages"); registry.setApplicationDestinationPrefixes("/ws"); } } Controller for websocket: #Controller public class MessageController { #Autowired private Consumer consumer; #Autowired private Utils utils; #Autowired private PersonService personService; #Autowired SimpMessagingTemplate simpMessagingTemplate; String destination = "/topic/messages"; ExecutorService executorService = Executors.newFixedThreadPool(1); Future<?> submittedTask; #MessageMapping("/start") public void startTask(Principal principal){ // Here, I would like to get the logged in user // If I use principal like this: principal.getName() => NullPointerException if ( submittedTask != null ){ simpMessagingTemplate.convertAndSend(destination, "Task already started"); return; } simpMessagingTemplate.convertAndSendToUser(sha.getUser().getName(), destination, "Started task"); submittedTask = executorService.submit(() -> { while(true){ simpMessagingTemplate.convertAndSend(destination, // "The calculated value " + val + " is equal to : " + max); } }); } How can I get the authenticated user? I needed it to check when to start the task for the web socket
Try to implement ChannelInterceptor, that need to be registrated in Config file (class that implements WebSocketMessageBrokerConfigurer) #Configuration #EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { private final ChannelInterceptor serverPushInBoundInterceptor; #Autowired public WebSocketConfig(#Qualifier("serverPushInBoundInterceptor") ChannelInterceptor serverPushInBoundInterceptor) { this.serverPushInBoundInterceptor = serverPushInBoundInterceptor; } .... #Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(serverPushInBoundInterceptor); } } #Component("serverPushInBoundInterceptor") public class ServerPushInBoundInterceptor implements ChannelInterceptor { private static final Logger log = LoggerFactory.getLogger(ServerPushInBoundInterceptor.class); #Override #SuppressWarnings("NullableProblems") public Message<?> postReceive(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (StompCommand.CONNECT.equals(Objects.requireNonNull(accessor).getCommand())) { List<String> authorization = accessor.getNativeHeader("Authorization"); if (authorization != null && !authorization.isEmpty()) { String auth = authorization.get(0).split(" ")[1]; System.out.println(auth); try { // find Principal Principal principal = ... accessor.setUser(new UsernamePasswordAuthenticationToken(principal, principal.getCredentials(), principal.getAuthorities())); } catch (Exception exc) { log.error("preSend", exc); } } } return message; } }
Spring WebFlux: WebSocketSession.send() doesn't send messages
Helloc I am trying to create webscoket endpoint using Spring WebFlux. I want this endpoint to return some events. In order to do so I created ConnectableFlux of events and in handle(..) method I map it to Flux. But after I give it to WebSocketSession, nothing happens - websocket client doesn't receive anything. But at the same time println(event.toString()) which you can see in my handle(..) method below actually prints information to console. Could you please tell what am I missing? public class EventWebsocketHandler implements WebSocketHandler { // constructors and etc. #Override public Mono<Void> handle(WebSocketSession session) { ObjectMapper objectMapper = new ObjectMapper(); Flux<WebSocketMessage> messages = eventService.events() .flatMap(event -> { try { System.out.println(event.toString()); return Mono.just(objectMapper.writeValueAsString(event)); } catch (JsonProcessingException e) { return Mono.error(e); } }) .map(session::textMessage); return session.send(messages); } #Service public class EventService { List<EventDto> events = new ArrayList<>(); private final Flux<EventDto> eventFlux = Flux.<EventDto>create(fluxSink -> { while (true) { if (!events.isEmpty()) { fluxSink.next(events.get(0)); events.remove(0); } } }) .publish() .autoConnect(); public void push(EventDto event) { events.add(event); } public Flux<EventDto> events() { return eventFlux; } } I have another WebSocketHandler in my project and it works fine, which means everything is OK with configuration: public class MyWebSocketHandler implements WebSocketHandler { #Override public Mono<Void> handle(WebSocketSession session) { Flux<Long> source = Flux.interval(Duration.ofMillis(1000 * 3)); return session.send(source.map(l -> session.textMessage(String.valueOf(l)))); } }
This private final Flux<EventDto> eventFlux = Flux.<EventDto>create(fluxSink -> { while (true) { if (!events.isEmpty()) { fluxSink.next(events.get(0)); events.remove(0); } } }) .publish() .autoConnect(); Has to be replaces with this private final Sinks.Many<EventDto> processor = Sinks.many().multicast().onBackpressureBuffer();
Spring boot's convertAndSendToUser not working
I'm new to Spring boot and trying to learn it. I'm following a tutorial on how to make a simple one-to-one chat app. Everything is working fine except that messages aren't getting sent between users. Messages get from the sender to the server but when convertAndSentToUser() nothing gets to the other client (recipient). Here's the message broker configuration: #Configuration #EnableWebSocketMessageBroker public class WebSocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer { #Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/user"); config.enableSimpleBroker("/topic", "/queue"); config.setApplicationDestinationPrefixes("/app"); config.setUserDestinationPrefix("/user"); } #Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/broadcast"); registry.addEndpoint("/broadcast").withSockJS().setHeartbeatTime(60_000); registry.addEndpoint("/chat").withSockJS(); } #Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new UserInterceptor()); } } And the controller that handles new connections: #RestController #Log4j2 public class WebSocketConnectionRestController { #Autowired private ActiveUserManager activeSessionManager; ... #PostMapping("/rest/user-connect") public String userConnect(HttpServletRequest request, #ModelAttribute("username") String userName) { String remoteAddr = ""; if (request != null) { remoteAddr = request.getHeader("Remote_Addr"); if (remoteAddr == null || remoteAddr.isEmpty()) { remoteAddr = request.getHeader("X-FORWARDED-FOR"); if (remoteAddr == null || "".equals(remoteAddr)) { remoteAddr = request.getRemoteAddr(); } } } log.info("registered " + userName + " " + remoteAddr); activeSessionManager.add(userName, remoteAddr); return remoteAddr; } ... } Finally, here's the controller that handles new messages: #Controller #Log4j2 public class WebSocketChatController implements ActiveUserChangeListener { #Autowired private SimpMessagingTemplate webSocket; ... #MessageMapping("/chat") public void send(#Payload ChatMessage chatMessage) throws Exception { ChatMessage message = new ChatMessage(chatMessage.getFrom(), chatMessage.getText(), chatMessage.getRecipient()); log.info("sent message to " + chatMessage.getRecipient()); webSocket.convertAndSendToUser(chatMessage.getRecipient(), "/queue/messages", message); } ... }
I finally found the problem. The tutorial had a UserInterceptor which links the session id for new connections to the username but it was trying to cast an Arraylist to a LinkedList so I just casted it to a List instead: public class UserInterceptor implements ChannelInterceptor { #Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (StompCommand.CONNECT.equals(accessor.getCommand())) { Object raw = message .getHeaders() .get(SimpMessageHeaderAccessor.NATIVE_HEADERS); if (raw instanceof Map) { Object name = ((Map) raw).get("username"); if (name instanceof List) { accessor.setUser(new User(((List) name).get(0).toString())); } } } return message; } }
Principal is null for every Spring websocket event
I'm trying to get the the Principal user name from Spring websocket SessionConnectEvent but it is null on every listener. What I can be doing wrong? To implement it I followed the answers you will find here: how to capture connection event in my webSocket server with Spring 4? #Slf4j #Service public class SessionEventListener { #EventListener private void handleSessionConnect(SessionConnectEvent event) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage()); String sessionId = headers.getSessionId(); log.debug("sessionId is " + sessionId); String username = headers.getUser().getName(); // headers.getUser() is null log.debug("username is " + username); } #EventListener private void handleSessionConnected(SessionConnectEvent event) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage()); String sessionId = headers.getSessionId(); log.debug("sessionId is " + sessionId); String username = headers.getUser().getName(); // headers.getUser() is null log.debug("username is " + username); } #EventListener private void handleSubscribeEvent(SessionSubscribeEvent event) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage()); String sessionId = headers.getSessionId(); log.debug("sessionId is " + sessionId); String subscriptionId = headers.getSubscriptionId(); log.debug("subscriptionId is " + subscriptionId); String username = headers.getUser().getName(); // headers.getUser() is null log.debug("username is " + username); } #EventListener private void handleUnsubscribeEvent(SessionUnsubscribeEvent event) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage()); String sessionId = headers.getSessionId(); log.debug("sessionId is " + sessionId); String subscriptionId = headers.getSubscriptionId(); log.debug("subscriptionId is " + subscriptionId); String username = headers.getUser().getName(); // headers.getUser() is null log.debug("username is " + username); } #EventListener private void handleSessionDisconnect(SessionDisconnectEvent event) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage()); log.debug("sessionId is " + event.getSessionId()); String username = headers.getUser().getName(); // headers.getUser() is null log.debug("username is " + username); } } This is my security config: #Configuration #EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { #Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .permitAll() .and().csrf().disable(); } }
As I'm not implementing an authentication mechanisim, Spring has no enough information to provide a Principal user name. So what I had to do is to configure a HandshakeHandler that generates the Principal. #Configuration #EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { public static final String ENDPOINT_CONNECT = "/connect"; public static final String SUBSCRIBE_USER_PREFIX = "/private"; public static final String SUBSCRIBE_USER_REPLY = "/reply"; public static final String SUBSCRIBE_QUEUE = "/queue"; #Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(SUBSCRIBE_QUEUE, SUBSCRIBE_USER_REPLY); registry.setUserDestinationPrefix(SUBSCRIBE_USER_PREFIX); } #Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(ENDPOINT_CONNECT) // assign a random username as principal for each websocket client // this is needed to be able to communicate with a specific client .setHandshakeHandler(new AssignPrincipalHandshakeHandler()) .setAllowedOrigins("*"); } } /** * Assign a random username as principal for each websocket client. This is * needed to be able to communicate with a specific client. */ public class AssignPrincipalHandshakeHandler extends DefaultHandshakeHandler { private static final String ATTR_PRINCIPAL = "__principal__"; #Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) { final String name; if (!attributes.containsKey(ATTR_PRINCIPAL)) { name = generateRandomUsername(); attributes.put(ATTR_PRINCIPAL, name); } else { name = (String) attributes.get(ATTR_PRINCIPAL); } return new Principal() { #Override public String getName() { return name; } }; } private String generateRandomUsername() { RandomStringGenerator randomStringGenerator = new RandomStringGenerator.Builder() .withinRange('0', 'z') .filteredBy(CharacterPredicates.LETTERS, CharacterPredicates.DIGITS).build(); return randomStringGenerator.generate(32); } }
Looking into the implementation of AbstractSubProtocolEvent (the superclass of all the events you're interested in) you can see that the user is hold in a seperate field. So you can simply access the user by calling event.getUser(). You don't need to get it from the message. E.g. for the SessionConnectedEvent you can see that the user gets populated in the event but not the message. Update: You can only access the user when you authenticated the http upgrade. So you need to have a WebSecurityConfigurerAdapter that configures something like: #Configuration public static class UserWebSecurity extends WebSecurityConfigurerAdapter { #Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers() .antMatchers(WebsocketPaths.WEBSOCKET_HANDSHAKE_PREFIX); //You configured the path in WebSocketMessageBrokerConfigurer#registerStompEndpoints http .authorizeRequests() .anyRequest().authenticated(); } }