I created a websocket communication between my Front-End which is coded in Vuejs and my Back-end which is coded in Spring Boot. Any user would authenticate in the first place ( the authentication is done without the websockets and a JWT token is passed to the user ) .And then a candidate would start a test (it's an e-learning platform) and the questions would be passed through a full-duplex flow with websockets. The client which is the Vuejs instance would submit the answer and the back-end would give him the next question. Well, the entire flow works. But I couldn't figure out how to secure the websockets. I haven't used neither STOMP neither SockJS for this and actually I haven't found a single ressource about this.
What I'd like to do is make a check on the user's role before giving him the permission to connect onto the websocket.
If the user has the privileges to access then the connection is successful
else it's a forbidden error (403).
Here is my code, I've used the Spring Boot websocket-starter annotations #ÒnOpen,#OnMessage, #OnClose and #OnError.
Those are some pieces of Code that may help out in finding the solution :
WebsocketConfiguration :
#Configuration
public class WebsocketConfiguration {
#Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket.java(Contains the logic):
I removed the logic so that it doesn't become too much code to read.
public class WebSocket {
private Session session;
#Autowired
QuestionRepository questionRepository;
private static CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();
private static Map<String,Session> sessionPool = new HashMap<String,Session>();
#OnOpen
public void onOpen(Session session, #PathParam(value="tentativeId")String tentativeId) {
}
#OnClose
public void onClose() {
webSockets.remove(this);
System. out. println ("[websocket message] disconnected, total:"+webSockets. size ());
}
#OnMessage
public void onMessage(String message,#PathParam(value="tentativeId") String tentativeId) throws JsonMappingException, JsonProcessingException {
}
// This is a broadcast message.
public void sendAllMessage(String message) {
for(WebSocket webSocket : webSockets) {
System.out.println ("[websocket message] broadcast message:"+message);
try {
webSocket.session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// This is a single message
public void sendOneMessage(String tentativeId, String message) {
Session session = sessionPool.get(tentativeId);
if (session != null) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Related
I can configure sending messages to specific users with WebSocketMessageBrokerConfigurer and SimpleMessageBroker, it works well.
Problem:
When somebody subscribes on specific userId, I want to send the previous messages from db to that subscriber. But if there already exists a connection and subscription with same userId (e.g. user logs in from another device/browser), old subscriber will receive the initial messages from db too. And with every new subscriber with the same userId, older subscribers will receive the same messages.
So how to send the initial messages from db to specific session, not all sessions?
Is this even possible with STOMP protocol?
Here is the code:
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/message").setAllowedOrigins("*");
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/chatroom", "/user");
registry.setApplicationDestinationPrefixes("/app");
registry.setUserDestinationPrefix("/user");
}
}
public class MessageController {
#MessageMapping("/private-message")
private void receivePrivateMessage(
#Payload Message message,
Principal user,
#Header("simpSessionId") String sessionId) {
log.info("server received private message: " + message);
simpMessagingTemplate.convertAndSendToUser(message.getReceiverId().toString(), "/private-message", message);
}
}
public class EventSubscribeListener implements ApplicationListener<SessionSubscribeEvent> {
private final SimpMessagingTemplate simpMessagingTemplate;
#Override
public void onApplicationEvent(SessionSubscribeEvent event) {
log.info("Stomp subscribed");
TODO get messages from db and send initial messages from db to to specific session/subscriber
}
}
Only idea I had is that I could replace the userId based subscribing to sessionId based subscribing. But that would mean frontend has to get the sessionId and subscribe with it. I don't want FE to deal with that.
I have followed Quetion1 and Quetion2 from stack overflow to send messages to specific client, based on its sessionId but could not find success.
Below is my sample RestController class
#RestController
public class SpringSessionTestApi {
#Autowired
public SimpMessageSendingOperations messagingTemplate;
#MessageMapping("/messages")
public void greeting(HelloMessage message, SimpMessageHeaderAccessor headerAccessor) throws Exception {
String sessionId = (String) headerAccessor.getSessionAttributes().get("SPRING.SESSION.ID");
messagingTemplate.convertAndSendToUser(sessionId,"/queue/test",message, createHeaders(sessionId));
}
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
}
Session Id: when client sends createSession request, new spring sessionId is generated and same is stored in MongoDB as well. After that when client sends web socket connect request, same sessionId is received which was stored in mongoDb as expected. Till This everything is working fine.
Now my job is to send response back to the client based on the sessionId.
For that I have below web socket class:
#Configuration
#EnableScheduling
#EnableWebSocketMessageBroker
public class WebSocketConfig extends
AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> {
#Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/messages");
}
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
and the sample client code that I am using to connect is:
function connect() {
stompClient = Stomp.client('ws://localhost:8016/messages');
stompClient.debug = null;
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/user/queue/test', function (greeting) {
console.log("Hello "+greeting);
console.log("Greeting body "+JSON.parse(greeting.body));
});
});
}
Please help, Where I am doing wrong in this?
Thanks in Advance!
If you are using /user channel as you do, try to pass the user as stated here.
#MessageMapping("/messages")
public void greeting(HelloMessage message, SimpMessageHeaderAccessor headerAccessor, Principal principal)
throws Exception {
messagingTemplate.convertAndSendToUser(principal.getName(), "/queue/test", message);
}
I've found a full workable Spring Stomp Chat project in git, the link is here. You can refer to it.
https://gist.github.com/theotherian/9906304
I'm looking for better architecture solution. Currently we have following end-point:
/**
* Endpoint for frontend to be sure we are logged in
*/
#RequestMapping(value = "/is_auth")
public boolean getAuth() {
return true;
}
This end-point is covered by Spring Security and only authenticated users have access to it.
What is the best practice of making frontend aware of user authentication state?
It looks like you are using pooling to check the login status. Your controller method
#RequestMapping(value = "/is_auth")
public boolean getAuth() {
return true;
}
will never return false. So in general there is no need to have a return value in this case.
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/is_auth")
public void ping() {
// log ?
}
I believe the best solution would be a websocket connection between client and server. If you then implement a SessionListener, you can very easy send a login status to corresponding client if his session get expired:
//
// pseudo code
//
#Component
public class SessionListener implements HttpSessionListener {
private static final Logger logger = LoggerFactory.getLogger(SessionListener.class);
#Autowired
private IWebsocketService websocketService; // you own service here
#Override
public void sessionCreated(HttpSessionEvent se) {
logger.debug("sessionCreated: {}", se.getSession().getId());
}
#Override
public void sessionDestroyed(HttpSessionEvent se) {
String sessionId = se.getSession().getId();
logger.debug("sessionDestroyed: {}", sessionId);
websocketService.sendLoginStatus(sessionId, false);
}
}
EDIT: here is a very good example how to implement websockets with spring and javascript: Using WebSocket to build an interactive web application
In javax websockets we can use something like the follows
Session.getAsyncRemote().sendText(String text)
Session.getBasicRemote().sendText();
How can we send an asynchronous messages using spring websocket.
From WebSocketSession of spring webscockets can we extract RemoteEndPoint and send an async messages
PS Note: I am using Basic Spring websockets...
The configuration and code is as follows:
#Configuration
#EnableWebMvc
#EnableAspectJAutoProxy
#EnableWebSocket
public class WebMVCConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
private static final String ENDPOINT_URL = "/echo";
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(socketHandler(), ENDPOINT_URL).setAllowedOrigins("*");
}
#Bean
public WebSocketHandler socketHandler() {
return new WebSocketTestHandler();
}
#Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
#Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
public class SpringMVCInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { ApplicationConfig.class, RabbitMQConfig.class, RabbitConnectionFactory.class,
WebPropertyPlaceHolderConfig.class};
}
#Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
#Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
}
#Configuration
public class WebSocketTestHandler extends TextWebSocketHandler {
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("Connection is established to Server....:: Session Open : {}", session.isOpen());
}
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
}
#Override
public void afterConnectionClosed(WebSocketSession curSession, CloseStatus status) throws Exception {
}
}
So inside handleTextMessage(WebSocketSession session,TextMessage message) {
Inside this method am creating multiple threads And sending same session Object and some other parameters..Inside each thread am not modifying any session object related parameters but am trying to execute
TextMessage socketMessage = new TextMessage(message);
session.sendMessage(socketMessage);
}
So each thread is trying to send messages using same session Object..But am facing the following error
java.lang.IllegalStateException: Blocking message pending 10000 for BLOCKING
at org.eclipse.jetty.websocket.common.WebSocketRemoteEndpoint.lockMsg(WebSocketRemoteEndpoint.java:130) ~[websocket-common-9.3.8.v20160314.jar:9.3.8.v20160314]
at org.eclipse.jetty.websocket.common.WebSocketRemoteEndpoint.sendString(WebSocketRemoteEndpoint.java:379) ~[websocket-common-9.3.8.v20160314.jar:9.3.8.v20160314]
at org.springframework.web.socket.adapter.jetty.JettyWebSocketSession.sendTextMessage(JettyWebSocketSession.java:188) ~[spring-websocket-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.web.socket.adapter.AbstractWebSocketSession.sendMessage(AbstractWebSocketSession.java:105) ~[spring-websocket-4.2.4.RELEASE.jar:4.2.4.RELEASE]
So is it possible to send asynchronous messages using spring websockets?
If yes please let me know what configuration changes are required in the above code..Or Can we extract the core AsyncRemoteEndPoint and BasicRemoteEndpoint from spring Websocket Session and can we send asynchronous messages..or if not both the above cases ..move the code to common place and put synchonized(sessionObject)
{
sendmessage
}..Sorry if the framing of question is not clear or already a duplicate question
Please note I am not using any Stomp client or anyother features over spring websocket..Am using plain spring websockets..And is it possible to do without using Future(java feature)(If yes..it would be better)?
I used ConcurrentWebSocketSessionDecorator on the session.
according to:
https://jira.spring.io/browse/SPR-13602
The decorator "enforces sending messages one at a time with a send buffer and send time limit per session. That helps quite a bit to limit the impact of slow clients"
I am implementing a Spring Boot + WebSocket + SockJS application and have a doubt about how to handle the HTTP session/ Websocket relation.
Basically I would like to be able to inform the user that his session has been invalidated (because of a timeout or because of having logged in from another location). For that I wanted to put in his own socket a certain message just before the framework closes it.
I turned to an http listener but the problem I have is that by the time HttpSessionListener.sessionDestroyed() is called I can see the socket has been already closed by the framework (not due to some other event like the user closing the browser).
Has anybody any idea about how to achieve this?
I have a really simple Websocket config and am running with Spring Boot defaults.
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(10000000);
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app/");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/myEndPoint").withSockJS();
}
}
Security part:
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
applySecurity(messages);
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
private static void applySecurity(MessageSecurityMetadataSourceRegistry messages) {
messages.nullDestMatcher().authenticated() //
.simpDestMatchers("/app/**").authenticated().simpSubscribeDestMatchers("/user/reply").authenticated()
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll().anyMessage().denyAll();
}
}
My Http listener would be like this:
return new HttpSessionListener() {
#Override
public void sessionDestroyed(HttpSessionEvent se) {
simpMessagingTemplate.convertAndSendToUser(....);
}
#Override
public void sessionCreated(HttpSessionEvent se) {
// no need to do anything when a session is created
}
};
UPDATE:
Spring-session handles issues like this one and many others.
HttpSession and Websocket session are two different kind of sessions. HttpSession is created when client accesses the url and completes Handshake whereas Websocket session is created when client sends subscribe request.
Websocket session is wrapped under HttpSession. If HttpSession gets timed out or invalidated then underlying Websocket session also gets disconnected, however, vice versa is not true.
Coming to your point of configuring listener for Websocket session, HttpSession listener won't be able to listen for Websocket session events. For such events, we need to define a class which extends org.springframework.web.socket.messaging.SubProtocolWebSocketHandler class and override afterConnectionEstablished and afterConnectionClosed methods. Have a look at Javadoc here.
Also, here is an example that listens for Spring websocket disconnect events. You need to configure something similar to this.
An HTTP session is supposed to survive the connection. That's what it's for. Multiple connections, same session. The session should not expire while there are active connections.