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
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;
}
}
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.
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.
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.
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.