I'm using react-stomp as follows:
import React from 'react';
import SockJsClient from 'react-stomp';
render() {
return (
<div>
<SockJsClient url={MY_WS_ENDPOINT + this.wsCredentials}
topics={['/topics/all']}
onMessage={(msg) => { console.log(msg); }}
ref={ (client) => { this.clientRef = client }} />
</div>
);
}
}
The server is built using Springboot, and the configuration is:
security configuration:
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers(MY_WS_ENDPOINT + "/**").authenticated()
.anyMessage().authenticated();
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
WebSocket configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint(MY_WS_ENDPOINT)
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(final MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/")
.enableSimpleBroker(WS_SIMPLE_BROKER.toArray(new String[0]));
}
}
After a while, the token expires, and the client continue trying to update status from server getting 401 errors for ever. Is there any approach where I can redirect to login page once the first 401 error is received?
Thanks (sorry, quite noob with this technology).
Related
I'm using spring boot in my backend and vuejs in my front-end . I've defined everything in the guide yet I still don't get my project to work.
FrontEnd
connectToWS() {
this.socket = new Sock("http://localhost:8754/stomp-endpoint");
this.stompClient = Stomp.over(this.socket);
this.stompClient.connect(
{},
frame => {
this.connected = true;
console.log(frame);
this.stompClient.subscribe("/topic/greetings", tick => {
console.log(tick);
this.received_messages.push(JSON.parse(tick.body).content);
});
},
error => {
console.log(error);
this.connected = false;
}
);
},
Backend
WebSocketConfiguration Class :
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfiguration implements
WebSocketMessageBrokerConfigurer
{
#Override
public void registerStompEndpoints(StompEndpointRegistry registry)
{
// with sockjs
registry.addEndpoint("/stomp-endpoint")
.setAllowedOrigins("http://localhost:8081")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/topic");
}
}
WebSecurityConfigClass :
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/v*/registration/**").permitAll()
.antMatchers("/topic**").permitAll()
.antMatchers("/stomp-endpoint").permitAll()
UPDATE :
The error is actually coming out from the browser . It actually changes the route of the api because it has sockjs integrated into it. I couldn't figure out why the API always gets a /info?t=RandomNumber after it. I need the route to remain the same as I typed it in the front-end.
How can I stop the browser from updating my path. Thanks for helping !
My goal is to notify, if a player joins a team in real-time.
Since the player will be created in the controller, i want to notify the team about this change via socket.
This has been my first attempt in doing that:
#RestController
#RequestMapping("/api/secure/players/")
public class PlayerController {
#Autowired
private SimpMessagingTemplate simpMessageTemplate;
#PostMapping
public ResponseEntity<PlayerDTO> createPlayer() {
// ...
// data is being processed
// notify team about the change
simpMessageTemplate.convertAndSend("/teams/" + selectedTeam.getId() + "/", player);
return ...
}
}
#Controller
public class TeamSocket {
private static final Logger logger = LoggerFactory.getLogger(TeamSocket.class);
#MessageMapping("/teams/{team}")
#SendTo("/teams/{team}")
public void teamEvent(#DestinationVariable String team) {
logger.debug("Received message -> {}", team);
}
}
#Configuration
#ComponentScan
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// I still dont fully understand this. Do i have to enable it? (It does not fix the issue).
// config.enableSimpleBroker("/teams/");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/api/secure/stomp/").withSockJS();
}
}
However, Spring tells me, that it was unable to map the request. What am i doing wrong?
DEBUG 29487 --- [nboundChannel-4] .WebSocketAnnotationMethodMessageHandler : Searching methods to handle SUBSCRIBE /teams/16 id=sub-0 session=0nkqv1km, lookupDestination='/teams/16'
DEBUG 29487 --- [nboundChannel-4] .WebSocketAnnotationMethodMessageHandler : No matching message handler methods.
DEBUG 29487 --- [nboundChannel-2] org.springframework.web.SimpLogging : Processing SUBSCRIBE /teams/16 id=sub-0 session=0nkqv1km
const client = new SockJS(`${REST_API}/api/secure/stomp/`);
const stompClient = Stomp.over(client);
stompClient.connect({}, (frame) => {
stompClient.subscribe(`/teams/${teamId}`, (message) => {
...
});
});
I see you are using path "/teams/" + selectedTeam.getId() + "/" in your convertAndSend method while you are subscribing to /teams/${teamId}. Do you need to add additional slash while subscribing or you may need to remove additional slash from convertAndSend?
I have implemented websocket in spring but the JavaScript client cannot connect to the websocket.
Here is the WebSocketConfig class:
package com.myapp.spring.security.config;
import com.myapp.spring.web.controller.MyWebSocketHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
//import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
//import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.handler.PerConnectionWebSocketHandler;
#Configuration
#EnableWebMvc
#EnableWebSocket
#ComponentScan(basePackages={"com.myapp.spring.*"})
public class WebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
.addHandler(myWebSocketHandler(), "/endpoint")
.setAllowedOrigins("*");
}
#Bean
public WebSocketHandler myWebSocketHandler() {
return new PerConnectionWebSocketHandler(MyWebSocketHandler.class);
}
}
Here is the test.html page that tries to connect to the websocket:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<body>
<p>Going to connect to the WebSocket at /endpoint. Check Console</p>
<button onclick="ss()">Send</button>
</body>
<script type="text/javascript">
var webSocket = new WebSocket("wss://"+document.location.hostname+":8443"+"/endpoint");
webSocket.onopen = function(message) {
processOpen(message);
};
webSocket.onmessage = function(message) {
processMessage(message);
};
webSocket.onclose = function(message) {
processClose(message);
};
webSocket.onerror = function(message) {
processError(message);
};
function processOpen(message) {
console.log("JS: Server Connected... "+message);
}
function processMessage(message) {
console.log("Getting a mess: "+message);
}
function processClose(message) {
console.log("JS: Client disconnected... "+message);
}
function processError(message) { //
console.log("Error occured: "+message);
}
function ss() {
webSocket.send("test");
}
</script>
</html>
I initialized the websocket path to be at /endpoint. This is evident by my server logs which say that this has occurred:
[org.springframework.web.socket.server.support.WebSocketHandlerMapping] (ServerService Thread Pool -- 75) Mapped URL path [/endpoint] onto handler of type [class org.springframework.web.socket.server.support.WebSocketHttpRequestHandler]
When I open up test.html, the connection opens and immediately disconnects. The processOpen(message) and processClose(message) function are immediately called, one after the other. So what am I doing wrong, and how can I fix this?
Your java-script code in test.html looks fine. There could be some issue with Spring Web socket configuration which is closing the connection. Following is the working code for web socket with spring boot. Please compare with your configuration.
Web socket dependency in pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketHandler class
#Component
public class EchoHandler extends TextWebSocketHandler {
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
TextMessage echoMessage = new TextMessage("Echo :" + message.getPayload());
System.out.println("Sending "+echoMessage.getPayload());
session.sendMessage(echoMessage);
}
}
WebSocket Controller class
#EnableWebSocket
#Controller
public class WSController implements WebSocketConfigurer {
#Autowired
private EchoHandler echoHandler;
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler, "/echo").setAllowedOrigins("*");
}
}
Spring Boot application class
#SpringBootApplication
public class WSApplication {
public static void main(String[] args) {
SpringApplication.run(WSApplication.class, args);
}
}
I am new in AngularJS as well as FullStack development. The architecture of my current app is already set up and should not change preferably (for security reasons). So far, I can emit messages to the server using angular-websocket-service. Here is the code snippet of the service from the front-end:
proxiMiamApp.service('WebSocketService', function ($websocket) {
var wsEndpoint = {};
this.openWsEndpoint = function () {
wsEndpoint = $websocket.connect("ws://localhost:9000/proximiamHandler");
console.log(wsEndpoint);
return wsEndpoint;
}
this.sendMessage = function(){
if($.isEmptyObject(this.wsEndpoint)){
this.openWsEndpoint();
}
eventUser = {
idEvent : '1',
idUser : '49'
};
wsEndpoint.register('/eventUser', function(){
console.log('Register OK!');
});
console.log('Ready!');
wsEndpoint.emit('/eventUser',eventUser);
}});
As for the back-end, I am using an implementation of the WebSocketHandler interface:
#Controller
public class ProximiamHandler implements WebSocketHandler {
#Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
System.out.println("afterConntectionEstablished called");
}
#Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
System.out.println("handleMessage called");
// My code here...
}
#Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
System.out.println("handleTransportError called");
}
#Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
System.out.println("afterConnectionClosed called");
}
#Override
public boolean supportsPartialMessages() {
return true;
}}
The Implementation of the WebSocketHandler is called via Spring WebSocketConfigurer
#Configuration
#EnableWebSocket
#Controller
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/proximiamHandler").setAllowedOrigins("*");
}
#Bean
public WebSocketHandler myHandler() {
return new ProximiamHandler();
}}
My questions are:
Can I notify subscribed clients using this architecture?
If yes, how can I do it?
Is there a way to return something to subscribed clients from the server? (an Object or a String for instance)
Thanks in advance for your help
Can I notify subscribed clients using this architecture?
=> Yes.
If yes, how can I do it?
=> Based on the Spring web socket APIs, you have to retain the ' WebSocketSession' passed to you via "afterConnectionEstablished" callback.
Use the sendMessage() API of Web socket session to send notifications to client.
Is there a way to return something to subscribed clients from the server? (an Object or a String for instance)
=> You can format your data in either JSON or XML & wrap it using "WebSocketMessage" and pass it to client.
I never worked on spring, however, I am answering this based on my knowledge on web socket. See if it helps.
I am using Spring-Boot 1.1.7, with spring security, html (no thyme-leaf) and javascript. I am unable to get my login to work correctly when I use javascript to submit my login. When I use html with a form, spring-security picks up the requests, authenticates and proceeds happily. But with Javascript, I get a 302 redirect and no authentication.
Here is my configuration:
#ComponentScan
#EnableAutoConfiguration
#EnableGlobalMethodSecurity(securedEnabled = true)
public class MyApplication {
public static void main(String[] args) {
ApplicationContext edm = SpringApplication.run( MyApplication.class, args );
}
}
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/menu").permitAll()
.antMatchers("/error").permitAll()
.antMatchers("/resources/**").permitAll()
.antMatchers("/css/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/fonts/**").permitAll()
.antMatchers("/libs/**").permitAll();
http
.formLogin().failureUrl("/login?error")
.defaultSuccessUrl("/")
.permitAll()
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/")
.permitAll();
http
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/login?expired")
.maxSessionsPreventsLogin(true)
.and()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/");
http
.authorizeRequests().anyRequest().authenticated();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder encoder = new BCryptPasswordEncoder();
auth.userDetailsService( customUserDetailsService ).passwordEncoder( encoder );
}
#Override
public void configure(WebSecurity security){
security.ignoring().antMatchers("/css/**","/fonts/**","/libs/**");
}
}
And I have my own UserDetailsService
#Component
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserService userService;
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userService.findByUsername( userName );
if ( user == null ) {
throw new UsernameNotFoundException( "UserName " + userName + " not found" );
}
return user;
}
}
And finally, here is the javascript that submits the post:
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$("#login").click(function(){
username=$("#username").val();
password=$("#password").val();
$.ajax({
type: "POST",
url: "/login",
beforeSend: function(xhr){
xhr.setRequestHeader(header, token);
},
data: "username="+username+"&password="+password,
success: function(html){
alert("logged in");
}
});
return false;
});
If I make this call from the url /login, I get the spring provided login form and all works perfectly. but I have a need to use javascript so I am wondering if there is something different I need to do to tell spring to look for it?
Here is the answer/solution that worked for me. Based on this article (http://patrickgrimard.com/2014/01/03/spring-security-csrf-protection-in-a-backbone-single-page-app/), adding a CSRFTokenGeneratorFilter extends OncePerRequestFilter and wiring it up to my security config, allowed my javascript provided parameters to be used.
public final class CSRFTokenGeneratorFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
response.setHeader("X-CSRF-HEADER", token.getHeaderName());
response.setHeader("X-CSRF-PARAM", token.getParameterName());
response.setHeader("X-CSRF-TOKEN", token.getToken());
filterChain.doFilter(request, response);
}
}
with wiring as:
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new CSRFTokenGeneratorFilter(), CsrfFilter.class)
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
..}
}
I am not sure why the filter is needed, but I guess spring-boot/security doesn't use that as a default.
302 probably means your request either has invalid credentials or you are hitting filter protected area of the app. Are you sure that /login is the url spring filter listens to? I can't see where you set your request path for spring to listen to.. The default if I recall correctly is /j_spring_security_check with j_username and j_password.
Try to override login processing url.
Taking config from our prod as an example:
http
.formLogin()
.loginProcessingUrl("/app/authentication")
.successHandler(ajaxAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.usernameParameter("j_username")
.passwordParameter("j_password")
.permitAll()
Hope this helps.