Controller component issue with Spring and SockJs - spring

I have a problem with a configuration of a WebSocket using Spring and SockJs.
I have configured all my app like Spring Documentation, the connection seems to be fine, but when I send a message the Controller is never involved and it doesn't work.
This is the Configuration component:
#Configuration
#EnableWebMvc
#EnableWebSocketMessageBroker
public class MyWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer implements WebSocketConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/mySocket").withSockJS().setInterceptors(new MySocketHandshakeInterceptor());;
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app").enableSimpleBroker("/topic");
}
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
}
}
This is the HandshakeInterceptor implementation:
public class MySocketHandshakeInterceptor implements HandshakeInterceptor {
#Override
public void afterHandshake(ServerHttpRequest paramServerHttpRequest, ServerHttpResponse paramServerHttpResponse, WebSocketHandler paramWebSocketHandler, Exception paramException) {
// TODO Auto-generated method stub
System.out.println("MySocketHandshakeInterceptor afterHandshake");
}
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> paramMap)
throws Exception {
System.out.println("MySocketHandshakeInterceptor beforeHandshake");
return true;
}
}
This is the Controller component:
#Controller
public class MySocketController {
#MessageMapping("/testsocket")
#SendTo("/topic/testresponse")
public MessageOut test(MessageIn message) throws Exception {
System.out.println("MySocketController start");
System.out.println("MySocketController test "+ message);
return new MessageOut("test OK");
}
}
These are MessageIn and MessageOut:
public class MessageIn {
private String test;
/**
* #return the test
*/
public String getTest() {
return test;
}
}
public class MessageOut {
private String result;
public MessageOut(String result) {
super();
this.result = result;
}
/**
* #return the test
*/
public String getResult() {
return result;
}
/**
* #param result the result to set
*/
public void setResult(String result) {
this.result = result;
}
}
Finally, this is the client side (javascript):
var socketSession = {};
connectToMySocket();
function connectToVideoSocket() {
socketSession.socket = new SockJS("/mySocket");
socketSession.socket.stomp = Stomp.over(socketSession.socket);
socketSession.socket.stomp.debug = null;
socketSession.socket.stomp.connect({}, function () {
socketSession.socket.stomp.subscribe("/topic/testresponse", function (data) {
console.log(data);
});
});
}
This is the command that I launch to test the socket:
socketSession.socket.stomp.send("app/testsocket", {}, JSON.stringify({'test': 'test'}));
In the system out console I can only see the two rows:
MySocketHandshakeInterceptor beforeHandshake
MySocketHandshakeInterceptor afterHandshake
The interceptor works fine, but I don't see any print by the Controller component.
What's wrong?
Thanks.

I resolved by myself.
It was a problem of the client-side.
The correct script is:
var mySocket = undefined;
var stompClient = undefined;
connectToVideoSocket();
function connectToVideoSocket() {
mySocket = new SockJS('/mySocket');
stompClient = Stomp.over(mySocket);
stompClient.debug = null;
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/testresponse', function(data){
console.log(JSON.parse(data.body).result);
});
});
}
In this way it works fine.

Related

Why should I use JSON.stringify() inside the STOMP send() method?

I'm trying to figure out how to write a simple chat on spring and Websocket.
I have simple configuration class :
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/hello").withSockJS();
}
}
and class controller ike this :
#Controller
public class GreetingController {
#MessageMapping("/hello")
#SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(3000); // simulated delay
return new Greeting("Hello, " + message.getName() + "!");
}
}
I understand that he gets requests via app/hello, but I don't understand why i have to use JSON.stringify({ 'name': name })call inside a js script?:
...
function sendName() {
var name = document.getElementById('name').value;
stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
}
...
That is, this function creates json and passes it to a function that performs deserialization, as I understand it, but can we do without it? or is it an obligatory condition?
Greeting class:
public class Greeting {
private String content;
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
HelloMessage class:
public class HelloMessage {
private String name;
public String getName() {
return name;
}
}

Can’t establish a connection to the socket in spring boot

I configured socket in my spring boot + angularjs application and you can see configuration classes here:
WebSocketBrokerConfig
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/looping")
.withSockJS()
.setClientLibraryUrl("https://cdn.jsdelivr.net/sockjs/1.1.2/sockjs.min.js");
}
}
WebSocketSecurityConfig
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().authenticated();
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
WebSocketAuthenticationConfig
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationConfig implements WebSocketMessageBrokerConfigurer {
private static final Logger logger = LoggerFactory.getLogger(WebSocketAuthenticationConfig.class);
#Autowired
private TokenProvider tokenProvider;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Autowired
private IUserService iUserService;
#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 (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwt = getJwtFromRequest(accessor);
logger.debug("X-Authorization: {}", jwt);
Long userId = tokenProvider.getUserIdFromToken(jwt);
User user = iUserService.loadById(userId);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
accessor.setUser(authentication);
}
return message;
}
});
}
private String getJwtFromRequest(StompHeaderAccessor accessor) {
String bearerToken = accessor.getFirstNativeHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
and this is client side:
function SocketController($timeout, localStorageService) {
var vm = this;
var accessToken = localStorageService.get('accessToken');
var stompClient;
console.log(accessToken)
if (accessToken) {
stompClient = webstomp.over(new WebSocket('ws://localhost:8000/looping'));
stompClient.connect({"Authorization": "Bearer " + accessToken}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/loops', function (message) {
console.log(message);
});
});
} else {
console.log("token expired");
}
}
In this case i see this below error in firefox devtools:
I also tried with SockJS as you can see below:
function SocketController($timeout, localStorageService) {
var vm = this;
var accessToken = localStorageService.get('accessToken');
var stompClient;
console.log(accessToken)
if (accessToken) {
var socket = new SockJS('https://localhost:8000/looping');
stompClient = Stomp.over(socket);
stompClient.connect({"Authorization": "Bearer " + accessToken}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/loops', function (message) {
console.log(message);
});
});
} else {
console.log("token expired");
}
}
And with SockJS i get 401 error when var socket = new SockJS('https://localhost:8000/looping'); is called.
NOTE
I using token oauth2 authorization and need to send token when handshaking for socket.

How i can pass new Object between Zuul filters, using spring context?

Currently, I have AuthFilter and here I received an UserState. I need to pass it to the next Filter. But how to do it right? Or exists other practices to resolve it?
public class AuthFilter extends ZuulFilter {
#Autowired
private AuthService authService;
#Autowired
private ApplicationContext appContext;
#Override
public String filterType() {
return PRE_TYPE;
}
#Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 2;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
String requestURI = context.getRequest().getRequestURI();
for (String authPath : authPaths) {
if (requestURI.contains(authPath)) return true;
}
return false;
}
#Override
public Object run() throws ZuulException {
try {
UserState userState = authService.getUserData();
DefaultListableBeanFactory context = new DefaultListableBeanFactory();
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(UserState.class);
beanDefinition.setPropertyValues(new MutablePropertyValues() {
{
add("user", userState);
}
});
context.registerBeanDefinition("userState", beanDefinition);
} catch (UndeclaredThrowableException e) {
if (e.getUndeclaredThrowable().getClass() == UnauthorizedException.class) {
throw new UnauthorizedException(e.getMessage());
}
if (e.getUndeclaredThrowable().getClass() == ForbiddenException.class) {
throw new ForbiddenException(e.getMessage(), "The user is not allowed to make this request");
}
}
return null;
}
}
I pretty sure filters are chained together and the request/response are passed through them. You can add the data to the request, and have the next filter look for it.

Spring Boot 4.3.5 WebSocket Chat with jwt authorization. No destination in GenericMessage

I'm trying to implement a 1-1 chat for a mobile app(ionic 3) with a spring boot back-end. Seems like run into some config problems.
Can't send message probably because the target channel wasn't created
Back-End:
ChatController:
#RestController
public class ChatController {
#Autowired
private PrivateChatService privateChatService;
private final static Logger logger = LogManager.getLogger(ChatController.class.getName());
#RequestMapping(value = "/chat/messages/{item_id}/chat_with/{buyer_login}", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<String> getExistingChatMessages(#PathVariable("item_id") String itemId, #PathVariable("buyer_login") String buyerLogin) {
List<ChatMessage> messages = privateChatService.getExistingChatMessages(itemId, buyerLogin);
logger.info("Here get messages");
return JSONResponseHelper.createResponse(messages, HttpStatus.OK);
}
#MessageMapping("/chat/{item_id}/send")
#SendTo("/topic/chat/{item_id}/chat_with/{buyer_login}")
public ChatMessage send(#Payload ChatMessage message,
#DestinationVariable("item_id") String item_id) throws Exception {
// logger.info(principal.getName());
logger.info(message.toString());
logger.info(item_id);
privateChatService.submitMessage(message);
return message;
}
}
WebSocketConfig:
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private final static Logger logger = LogManager.getLogger(WebSocketConfig.class.getName());
#Autowired
private JwtTokenProvider jwtTokenProvider;
#Autowired
private PrivateChatService privateChatService;
private static final String MESSAGE_PREFIX = "/topic";
private static final String END_POINT = "/chat";
private static final String APPLICATION_DESTINATION_PREFIX = "/live";
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
if (registry != null) {
registry.addEndpoint(END_POINT).setAllowedOrigins("*").withSockJS();
}
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
if (registry != null) {
registry.enableSimpleBroker(MESSAGE_PREFIX);
registry.setApplicationDestinationPrefixes(APPLICATION_DESTINATION_PREFIX);
}
}
#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 authToken = accessor.getFirstNativeHeader("Authentication");
String jwt = JwtUtils.resolveToken(authToken);
if (jwtTokenProvider.validateToken(jwt)) {
Authentication authentication = jwtTokenProvider.getAuthentication(jwt);
accessor.setUser(authentication);
String itemId = accessor.getFirstNativeHeader("item_id");
accessor.setDestination("/topic" + privateChatService.getChannelId(itemId, authentication.getName()));
logger.info(accessor.getDestination()); //ex: /topic/chat/3434/chat_with/user3797474342423
}
}
return message;
}
});
}
}
WebSocketSecurityConfig
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
Mobile client, ng2-stomp-service:
private _initWebsock(auth_token:string, item_id: number) {
let headers: Object = {
Authentication: `Bearer ${auth_token}`,
item_id: item_id
};
this.stomp.configure({
host :this.websocketApi + 'chat',
headers: headers,
queue:{'init':false}
});
console.log("Connecting stomp socket...");
//start connection
this.stomp.startConnect().then(() => {
this.stomp.done('init');
console.log('connected');
//subscribe
this.subscription = this.stomp.subscribe(`/chat/${item_id}/`, this.socketListener);
});
}
public socketListener = (data) => {
console.log(data)
};
send(msg: ChatMessage, item_id: number){
//send data
console.log(msg);
this.stomp.send(`/live/chat/${item_id}/send`, {}, JSON.stringify(msg));
}
Problem 1(probably):
In the browser console it shows that a client subscribes to /chat/item_id instead of /topic/chat/3434/chat_with/user3797474342423 => seems like configureClientInboundChannel doesn't work?
Problem 2:
When trying to execute this.stomp.send(/live/chat/${item_id}/send, {}, JSON.stringify(msg));, getting
o.s.m.s.b.DefaultSubscriptionRegistry : No destination in GenericMessage [payload=byte[2], headers={simpMessageType=MESSAGE.... Error.
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#websocket-stomp-authentication
and
https://stackoverflow.com/a/33962402/8336511
This is how I solved this problem:
When user authenticates with Spring Security, WebSocket module creates
unique channel for that user based on his Principal. Example
"/user/queue/position-updates" is translated to
"/queue/position-updates-user123"
So on the client side all I had to do, was subscribe to
/user/queue/requests
And on the server side, send messages to
/user/{username}/queue/requests with
convertAndSendToUser(request.getFromUser(), "/queue/requests",
request) and Spring handles the rest.

Webflux doesn't support ResponseBodyAdvice yet?

Since ResponseBodyAdvice interface is in web.servlet package
How could I implement such functions in webflux?
I also have this problem, i found it can be done by HandlerResultHandler.
For Example, I extend ResponseBodyResultHandler to wrap all my response
First, you should write ResponseWrapper.java
public class ResponseWrapper extends ResponseBodyResultHandler {
private static MethodParameter param;
static {
try {
//get new params
param = new MethodParameter(ResponseWrapper.class
.getDeclaredMethod("methodForParams"), -1);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public ResponseWrapper(List<HttpMessageWriter<?>> writers, RequestedContentTypeResolver resolver) {
super(writers, resolver);
}
private static Mono<Response> methodForParams() {
return null;
}
#Override
public boolean supports(HandlerResult result) {
boolean isMono = result.getReturnType().resolve() == Mono.class;
boolean isAlreadyResponse = result.getReturnType().resolveGeneric(0) == Response.class;
return isMono && !isAlreadyResponse;
}
#Override
#SuppressWarnings("unchecked")
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
Preconditions.checkNotNull(result.getReturnValue(), "response is null!");
// modify the result as you want
Mono<Response> body = ((Mono<Object>) result.getReturnValue()).map(Response::success)
.defaultIfEmpty(Response.success());
return writeBody(body, param, exchange);
}
}
Then, add Bean in SpringBootApplication
#SpringBootApplication
public class SmartApplication {
#Autowired
ServerCodecConfigurer serverCodecConfigurer;
#Autowired
RequestedContentTypeResolver requestedContentTypeResolver;
#Bean
ResponseWrapper responseWrapper() {
return new ResponseWrapper(serverCodecConfigurer
.getWriters(), requestedContentTypeResolver);
}
//Spring start
public static void main(String[] args) {
SpringApplication.run(SmartApplication.class, args);
}
}
My response class, for reference
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Response<T> {
boolean success;
T data;
Object error;
String warning;
public Response() {
}
public static Response success() {
return success(null);
}
public static <T> Response success(T data) {
return new Response<T>().setSuccess(true).setData(data);
}
public static Response error(Throwable e) {
LogManager.getLogger(StackLocatorUtil.getCallerClass(2)).info(e);
return new Response().setSuccess(false).setError(e);
}
public static Response error(Object e) {
return new Response().setSuccess(false).setError(e);
}
public static Response error(String e) {
return new Response().setSuccess(false).setError(e);
}
/* get success */
public boolean isSuccess() {
return success;
}
/* set success */
public Response setSuccess(boolean success) {
this.success = success;
return this;
}
/* get data */
public Object getData() {
return data;
}
/* set data */
public Response setData(T data) {
this.data = data;
return this;
}
/* get error */
public Object getError() {
return error;
}
/* set error */
public Response setError(Object error) {
this.error = error;
return this;
}
}
i used ResponseBodyAdvise before spring 5.0. i think ResponseBodyResultHandler suport webflux since spring5.0.

Resources