Websocket not working with spring boot application and angular frontend - spring-boot

I looked and tried a lot but I can not find the cause of my problem...
I have a JHipster generated application which consists out of a spring boot application and an angular frontend and I want to use websockets for updates. For that I use Stomp and SockJs
The connection itself is already not working.
I get the following error:
WebSocket connection to 'ws://localhost:9000/updates/websocket/447/xxudq4ni/websocket' failed: WebSocket is closed before the connection is established.
This is the call to port 9000, which is then proxied to the actual backend under Port 8080.
If I call the backend under port 8080 directly, I get:
WebSocket connection to 'ws://localhost:8080/updates/websocket/156/mg0dspp2/websocket' failed: Error during WebSocket handshake: Unexpected response code: 200
I do not really see what the actual response is but I suppose it is the JHIpster error message "an error has occured" and this html is returned with a http statuscode of 200.
I'm out of ideas what the actual problem is... I followed this intro here and several others...
here is my backend:
WebsocketConfig:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public static final String IP_ADDRESS = "IP_ADDRESS";
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/updates/websocket")
//.setHandshakeHandler(defaultHandshakeHandler())
.setAllowedOrigins("*")
.withSockJS()
.setClientLibraryUrl("https://cdn.jsdelivr.net/npm/sockjs-client#1.5.0/dist/sockjs.min.js");
//.setInterceptors(httpSessionHandshakeInterceptor());
}
private DefaultHandshakeHandler defaultHandshakeHandler() {
return new DefaultHandshakeHandler() {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS));
principal = new AnonymousAuthenticationToken("WebsocketConfiguration", "anonymous", authorities);
}
return principal;
}
};
}
#Bean
public HandshakeInterceptor httpSessionHandshakeInterceptor() {
return new HandshakeInterceptor() {
#Override
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes
) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress());
}
return true;
}
#Override
public void afterHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception
) {}
};
}
}
Controller:
#Controller
public class UpdateController {
private static final Logger log = LoggerFactory.getLogger(UpdateController.class);
#MessageMapping("/updates/websocket")
#SendTo("/topic/trucks")
public UpdateDto send(UpdateDto dto) {
return dto;
}
}
Frontend:
connect(): void {
if (this.stompClient?.connected || this.called) {
return;
}
this.called = true;
// building absolute path so that websocket doesn't fail when deploying with a context path
let url = '/updates/websocket';
url = this.location.prepareExternalUrl(url);
var socket = new SockJS(url);
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, (frame) => {
this.connectionSubject.next();
this.sendActivity();
this.routerSubscription = this.router.events
.pipe(filter((event: Event) => event instanceof NavigationEnd))
.subscribe(() => this.sendActivity());
}, error => {
console.log(error);
});
}
Im on Windows and I use Chrome for the development. But it also does not work in FireFox, so I do not think it has something to do with the platform.
Any help would be very much appreciated. Thank you very much!

Related

Stomp java client auth headers are not send

when i try to send auth headers via stomp java fx client, they are not passed to the spring boot backend.
Client:
String URL = 'wss://myurl.de/wsconnection'
WebSocketClient client = new StandartWebSocketClient(client);
WebSocketStompClient stompClient = new WebSocketStompClient(client);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
StompSessionHandler sessionHandler = new MyStompSessionHandler();
StompHeaders headers = new Headers();
headers.set("Authorization", "Bearer myToken")
headers.setLogin("Bearer myToken")
stompClient.connect(URL, sessionHandler,headers);
I tried two ways: using the login header method and custom Authorization header. So in the backend i try to get a jwt through one of these two headers. Unfortunatly the jwt value is always null so not passed.
Backend:
#Configuration
public class WebSocketSecurityConf extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().permitAll();
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwtToken;
jwtToken = accessor.getFirstNativeHeader("Authorization");
if(jwtToken == null){
jwtToken = accessor.getFirstNativeHeader("login");
}
System.out.println("webSocket token is "+ jwtToken);
System.out.println("accessor header "+ accessor.getMessageHeaders());
Any ideas here?
You have mixed up the order of the parameters here: stompClient.connect(URL, sessionHandler,headers); Headers should be before handler.
Here is from spring sources:
public ListenableFuture<StompSession> connect(String url, #Nullable WebSocketHttpHeaders handshakeHeaders,
#Nullable StompHeaders connectHeaders, StompSessionHandler handler, Object... uriVariables)

Spring websocket establishing connection is stuck at 'opening connection'

I am using spring-boot-websocket (spring-boot version 1.5.10) in my project. I have configured it as below,
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport
implements WebSocketMessageBrokerConfigurer {
#Value( "${rabbitmq.host}" )
private String rabbitmqHost;
#Value( "${rabbitmq.stomp.port}" )
private int rabbitmqStompPort;
#Value( "${rabbitmq.username}" )
private String rabbitmqUserName;
#Value( "${rabbitmq.password}" )
private String rabbitmqPassword;
#Override
public void configureMessageBroker( MessageBrokerRegistry registry )
{
registry.enableStompBrokerRelay("/topic", "/queue").setRelayHost(rabbitmqHost).setRelayPort(rabbitmqStompPort)
.setSystemLogin(rabbitmqUserName).setSystemPasscode(rabbitmqPassword);
registry.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints( StompEndpointRegistry stompEndpointRegistry )
{
stompEndpointRegistry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
}
#Bean
#Override
public WebSocketHandler subProtocolWebSocketHandler()
{
return new CustomSubProtocolWebSocketHandler(clientInboundChannel(), clientOutboundChannel());
}
#Override
public void configureWebSocketTransport( WebSocketTransportRegistration registry )
{
super.configureWebSocketTransport(registry);
}
#Override
public boolean configureMessageConverters( List<MessageConverter> messageConverters )
{
return super.configureMessageConverters(messageConverters);
}
#Override
public void configureClientInboundChannel( ChannelRegistration registration )
{
super.configureClientInboundChannel(registration);
}
#Override
public void configureClientOutboundChannel( ChannelRegistration registration )
{
super.configureClientOutboundChannel(registration);
}
#Override
public void addArgumentResolvers( List<HandlerMethodArgumentResolver> argumentResolvers )
{
super.addArgumentResolvers(argumentResolvers);
}
#Override
public void addReturnValueHandlers( List<HandlerMethodReturnValueHandler> returnValueHandlers )
{
super.addReturnValueHandlers(returnValueHandlers);
}
}
public class CustomSubProtocolWebSocketHandler extends SubProtocolWebSocketHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomSubProtocolWebSocketHandler.class);
#Autowired
private UserCommons userCommons;
CustomSubProtocolWebSocketHandler(MessageChannel clientInboundChannel,
SubscribableChannel clientOutboundChannel) {
super(clientInboundChannel, clientOutboundChannel);
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
LOGGER.info("************************************************************************************************************************New webSocket connection was established: {}", session);
String token = session.getUri().getQuery().replace("token=", "");
try
{
String user = Jwts.parser().setSigningKey(TokenConstant.SECRET)
.parseClaimsJws(token.replace(TokenConstant.TOKEN_PREFIX, "")).getBody().getSubject();
Optional<UserModel> userModelOptional = userCommons.getUserByEmail(user);
if( !userModelOptional.isPresent() )
{
LOGGER.error(
"************************************************************************************************************************Invalid token is passed with web socket request");
throw new DataException(GeneralConstants.EXCEPTION, "Invalid user", HttpStatus.BAD_REQUEST);
}
}
catch( Exception e )
{
LOGGER.error(GeneralConstants.ERROR, e);
}
super.afterConnectionEstablished(session);
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
LOGGER.error("************************************************************************************************************************webSocket connection was closed");
LOGGER.error("Reason for closure {} Session: {} ", closeStatus.getReason(),session.getId() );
super.afterConnectionClosed(session, closeStatus);
}
#Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
LOGGER.error("************************************************************************************************************************Connection closed unexpectedly");
LOGGER.error(GeneralConstants.ERROR, exception);
super.handleTransportError(session, exception);
}
}
From the client-side, I am creating a SockJS object to establish the connection,
let url = `/ws?token=${localStorage.getItem("access_token")}`;
// Web Socket connection
/* eslint-disable */
let sockJS = new SockJS(url);
let stompClient = Stomp.over(sockJS);
debugger
this.setState({
stompObject : stompClient,
});
But the connection is not getting established consistently, most of the times it is stuck at Opening the connection, in the backend log, I can see the connection getting established and a session is created. But, in the browser console, I can see client-side sending message to the server but the server is not acknowledging the message.
Sometimes, when I refresh the browser for 10-15 times, the connection is getting established successfully. Is there any mistake in my configuration?
Thank You.
Given that you can "hit refresh 10 or 15 times and then get a connection," I'm curious if you dealing with a cookie issue? I know Chrome is famous for that sort of thing. Anyway close out all browser windows and stop the browser, then start the browser, and tell it to clear browsing history and then attempt the connection. Also, be SURE you read the version of the spring-boot docs for the version of spring-boot you are that you are actually using, and also specify the SB version in your questions and when looking for answers.

How to customize Spring boot controller API response for ZuulException

We are using Zuul, Eureka and spring boot application services for REST APIs.
Suppose my spring boot service is down and when I tried to access the API using Zuul API gateway, I am getting ZuulException and response is below :
{
"timestamp": "2018-10-12T14:29:09.632+0000",
"status": 500,
"error": "Internal Server Error",
"exception": "com.netflix.zuul.exception.ZuulException",
"message": "GENERAL"
}
I want to customize the response format like below:
{
"success": false,
"message": "Service is down. Please try later"
}
I tried to implement https://stackoverflow.com/a/39841785/5506061 but its not working for me.
Please suggest how to customize the response for ZuulException.
You can implement your own FallbackProvider and customize the response based on the cause if needed.
Something like :
#Component
public class CustomFallbackBasedOnCause implements FallbackProvider {
private static final String DEFAULT_MSG = "{\"success\": false,\"message\": \"Service is down. Please try later\"}";
#Override
public String getRoute() {
return "*"; // * = all routes
}
#Override
public ClientHttpResponse fallbackResponse(final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return fallbackResponse();
}
}
#Override
public ClientHttpResponse fallbackResponse() {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
#Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
#Override
public int getRawStatusCode() throws IOException {
return status.value();
}
#Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
#Override
public void close() {
}
#Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(DEFAULT_MSG.getBytes());
}
#Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
As you can see in the getRoute() method, you can specify if this customFallback will be used for all routes (return "*") or for a specific route.
In case you work with Registry service (e.g Eureka). You don’t specify the route URL but the service id instead. return "SERVICEID"

Spring MVC Websockets with STOMP - Authenticate against specific channels

Is there a way in AbstractWebSocketMessageBrokerConfigurer (Spring Boot) to intercept the registration of users to a specific channel?
I have a basic authentication done in registerStompEndpoints using a HandshakeHandler:
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
HandshakeHandler handler = new DefaultHandshakeHandler() {
#Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
return () -> getPrincipal();
}
return principal;
}
};
registry.addEndpoint("/websocket")
.setHandshakeHandler(handler)
.setAllowedOrigins("*").withSockJS();
}
Now I would like to prevent this user from registering to '/topic/admin/news' if the user does not have the permission 'admin'. I'm not using Spring Security. I'd like to have an interceptor before the registration to a channel happens.
As an alternative, I'd like to use the SimpMessagingTemplate to only send out messages to users from the channel that have the permission. Is there a way to see what users are currently connected to my stomp-connection?
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new TopicSubscriptionInterceptor());
}
And the interceptor:
public class TopicSubscriptionInterceptor implements ChannelInterceptor {
private static Logger logger = org.slf4j.LoggerFactory.getLogger(TopicSubscriptionInterceptor.class);
#Override
public Message<?> postReceive(Message<?> message, MessageChannel chanenel) {
return message;
}
#Override
public void postSend(Message<?> message, MessageChannel chanel, boolean sent) {
}
#Override
public boolean preReceive(MessageChannel channel) {
return true;
}
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor headerAccessor= StompHeaderAccessor.wrap(message);
if (StompCommand.SUBSCRIBE.equals(headerAccessor.getCommand()) && headerAccessor.getHeader("simpUser") !=null && headerAccessor.getHeader("simpUser") instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) headerAccessor.getHeader("simpUser");
if(!validateSubscription((User)userToken.getPrincipal(), headerAccessor.getDestination()))
{
throw new IllegalArgumentException("No permission for this topic");
}
}
return message;
}
private boolean validateSubscription(User principal, String topicDestination)
{
logger.debug("Validate subscription for {} to topic {}",principal.getUsername(),topicDestination);
//Validation logic coming here
return true;
}
}

How to add custom headers to STOMP CREATED message in Spring Boot application?

I'm trying to add custom headers to the STOMP 'CREATED' message, which is received by client at the first connection. Here is the function which connects to the WebSocket using STOMP JavaScript:
function connect() {
socket = new SockJS('/chat');
stompClient = Stomp.over(socket);
stompClient.connect('', '', function(frame) {
whoami = frame.headers['user-name'];
console.log(frame);
stompClient.subscribe('/user/queue/messages', function(message) {
console.log("MESSAGE RECEIVED:");
console.log(message);
showMessage(JSON.parse(message.body));
});
stompClient.subscribe('/topic/active', function(activeMembers) {
showActive(activeMembers);
});
});
}
This function prints the following to the browser's console:
body: ""
command: "CONNECTED"
headers: Object
heart-beat: "0,0"
user-name: "someuser"
version: "1.1"
And i want to add custom header so output must look like:
body: ""
command: "CONNECTED"
headers: Object
heart-beat: "0,0"
user-name: "someuser"
version: "1.1"
custom-header: "foo"
I have the following WebSocket configuration in my Spring Boot app.
WebSocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue", "/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat", "/activeUsers")
.withSockJS()
.setInterceptors(customHttpSessionHandshakeInterceptor());
}
...
#Bean
public CustomHttpSessionHandshakeInterceptor
customHttpSessionHandshakeInterceptor() {
return new CustomHttpSessionHandshakeInterceptor();
}
}
I have tried to register the 'HandshakeInterceptor' to set custom header, but it didn't work. Here is 'CustomHttpSessionHandshakeInterceptor':
CustomHttpSessionHandshakeInterceptor.java
public class CustomHttpSessionHandshakeInterceptor implements
HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest =
(ServletServerHttpRequest) request;
attributes.put("custom-header", "foo");
}
return true;
}
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception ex) { }
}
I have found this code snippet at https://dzone.com/articles/spring-boot-based-websocket
Can someone explain me why this approach does not work? Is there another way to set custom headers to the STOMP 'CREATED' message at server side in Spring Boot application?
Thanks!
Maybe it's too late, but better late than never ...
Server messages (e.g. CONNECTED) are immutable, means that they cannot be modified.
What I would do is register a client outbound interceptor and trap the connected message by overriding the preSend(...) method and build a new message with my custom headers.
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel)
{
LOGGER.info("Outbound channel pre send ...");
final StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
final StompCommand command = headerAccessor.getCommand();
if (!isNull(command)) {
switch (command) {
case CONNECTED:
final StompHeaderAccessor accessor = StompHeaderAccessor.create(headerAccessor.getCommand());
accessor.setSessionId(headerAccessor.getSessionId());
#SuppressWarnings("unchecked")
final MultiValueMap<String, String> nativeHeaders = (MultiValueMap<String, String>) headerAccessor.getHeader(StompHeaderAccessor.NATIVE_HEADERS);
accessor.addNativeHeaders(nativeHeaders);
// add custom headers
accessor.addNativeHeader("CUSTOM01", "CUSTOM01");
final Message<?> newMessage = MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders());
return newMessage;
default:
break;
}
}
return message;
}
#UPDATE:::
The interface needed is called ChannelInterceptor and to register your own implementation you need to add #Configuration annotated class
#Configuration
public class CustomMessageBrokerConfig extends WebSocketMessageBrokerConfigurationSupport
implements WebSocketMessageBrokerConfigurer{}
and override a method configureClientOutboundChannel as below
#Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
log.info("Configure client outbound channel started ...");
registration.interceptors(new CustomOutboundChannelInterceptor());
log.info("Configure client outbound channel completed ...");
}
Did you try it like this way? MessageHeaderAccessor has a setHeader method too.
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-authentication-token-based

Resources