web-dev-qa-db-fra.com

Comment sécuriser l'application websocket [Spring boot + STOMP]

Bonjour,

J'ai créé une simple application Spring Boot WebSocket. En ce moment, je voudrais mettre un peu de sécurité à ce sujet. J'ai essayé quelques exemples mais je ne peux pas le faire fonctionner. Je reçois une erreur:

navigateur web:

>>> CONNECT
${_csrf.headerName}:${_csrf.token}
accept-version:1.1,1.0
heart-beat:10000,10000

<<< ERROR
message:Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.web.csrf.MissingCsrfTokenException\c Could not verify the provided CSRF token because your session was not found.
content-length:0

Connectez-vous STS:

Failed to send client message to application via MessageChannel in session cc25e1mw. Sending STOMP ERROR to client.

Trace de la pile:

org.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.web.csrf.MissingCsrfTokenException: Could not verify the provided CSRF token because your session was not found.
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.Java:127) ~[spring-messaging-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.Java:104) ~[spring-messaging-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.Java:299) ~[spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.handleMessage(SubProtocolWebSocketHandler.Java:306) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.Java:75) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.Java:56) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.handleMessage(ExceptionWebSocketHandlerDecorator.Java:58) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession.delegateMessages(AbstractSockJsSession.Java:380) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession.handleMessage(WebSocketServerSockJsSession.Java:194) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler.handleTextMessage(SockJsWebSocketHandler.Java:92) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.Java:43) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.Java:110) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.Java:42) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.Java:81) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.Java:78) [spring-websocket-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.Apache.Tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.Java:395) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.Java:119) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.Java:495) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.WsFrameBase.processData(WsFrameBase.Java:294) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.Java:133) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.Java:82) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.Java:171) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.Java:151) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.Java:148) [Tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.Apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.Java:54) [Tomcat-embed-core-8.5.23.jar:8.5.23]
at org.Apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.Java:53) [Tomcat-embed-core-8.5.23.jar:8.5.23]
at org.Apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.Java:868) [Tomcat-embed-core-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.Java:1459) [Tomcat-embed-core-8.5.23.jar:8.5.23]
at org.Apache.Tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.Java:49) [Tomcat-embed-core-8.5.23.jar:8.5.23]
at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1149) [na:1.8.0_144]
at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:624) [na:1.8.0_144]
at org.Apache.Tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.Java:61) [Tomcat-embed-core-8.5.23.jar:8.5.23]
at Java.lang.Thread.run(Thread.Java:748) [na:1.8.0_144]
Caused by: org.springframework.security.web.csrf.MissingCsrfTokenException: Could not verify the provided CSRF token because your session was not found.
at org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor.preSend(CsrfChannelInterceptor.Java:55) ~[spring-security-messaging-4.2.3.RELEASE.jar:4.2.3.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.Java:158) ~[spring-messaging-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.Java:113) ~[spring-messaging-4.3.13.RELEASE.jar:4.3.13.RELEASE]
... 32 common frames omitted

Mes fichiers de configuration:

WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer{



@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/websocket").setHandshakeHandler(new MyHandshakeHandler()).setAllowedOrigins("*").withSockJS();
}

public class MyHandshakeHandler extends DefaultHandshakeHandler {

    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
            Map<String, Object> attributes) {
        // TODO Auto-generated method stub
        return super.determineUser(request, wsHandler, attributes);
    }


}
}

Fonction du contrôleur

@MessageMapping("/hello")
@SendTo("/topic/messaging")
public Message sendMessage(Message message) throws Exception {
    Thread.sleep(10); // simulated delay
    messageRepository.save(message);

    return new Message(message.getFromUserId(), message.getToUserId(), message.getMessageText(), "delivered", message.getDate());
}

Fonction JS à connecter:

function connect() {
var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";
var headers = {};
headers[headerName] = token;

var socket = new SockJS('/websocket');
stompClient = Stomp.over(socket);
stompClient.connect(headers, function (frame) {
    setConnected(true);
    console.log('Connected: ' + frame);
    stompClient.subscribe('/topic/messaging', function (message) {
        showMessage(JSON.parse(message.body).messageText);
    });
});
}

Donc, mon problème est: le navigateur Web se connecte à l'application et envoie un message (je ne sais pas s'il est sécurisé) mais il ne peut recevoir aucun message de l'application.

Ma question: Comment obtenir une connexion Websocket sécurisée et comment se débarrasser de cette erreur?.

Je suis novice en matière de sécurisation d'applications Web, alors soyez indulgent.

Merci pour tout conseil.

Andrew

4
Andrew

De la documentation:

Généralement, nous devons inclure le jeton CSRF dans un en-tête HTTP ou un paramètre HTTP. Cependant, SockJS ne permet pas ces options. Au lieu de cela, nous devons inclure le jeton dans les en-têtes Stomp

Ok, nous savons maintenant que nous devons inclure ces en-têtes dans les en-têtes Stomp. Si vous utilisez des JSP dans votre application, vous pouvez obtenir les en-têtes et les jetons CSRF à partir des attributs de requête côté client.

var headerName = "${_csrf.headerName}"; var token = "${_csrf.token}";

Si vous utilisez not et utilisez des fichiers JSP et utilisez du code HTML normal, vous devez exposer le jeton CsrfToken sur un noeud final REST, par exemple. à /csrf:

@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}

Il y a d'autres mises en garde. Par exemple, supposons que vous souhaitiez autoriser d'autres domaines à accéder à votre point de terminaison de socket Web. Dans votre WebSocketSecurityConfig, vous pouvez fournir les éléments suivants:

@Override
protected boolean sameOriginDisabled() {
    return true;
}

Une dernière chose importante que j'ai apprise est la suivante:

SockJS utilise un POST sur les messages CONNECT pour tout transport basé sur HTTP. Généralement, nous devons inclure le jeton CSRF dans un en-tête HTTP ou un paramètre HTTP. Cependant, SockJS ne permet pas ces options. Au lieu de cela, nous devons inclure le jeton dans les en-têtes Stomp, comme décrit à la Section 24.4.3, «Ajout de CSRF aux en-têtes Stomp».

Cela signifie également que nous devons assouplir notre protection CSRF avec la couche Web. Plus précisément, nous souhaitons désactiver la protection CSRF pour nos URL de connexion. Nous ne voulons PAS désactiver la protection CSRF pour chaque URL. Sinon, notre site sera vulnérable aux attaques CSRF.

Le deuxième paragraphe est la clé ici, en termes plus simples, vous devez ajouter ce qui suit à la classe qui a étendu WebSecurityConfigurerAdapter

    http
        .csrf()
            // ignore our stomp endpoints since they are protected using Stomp headers
            .ignoringAntMatchers("/chat/**")
            .and()
        .headers()
            // allow same Origin to frame our site to support iframe SockJS
            .frameOptions().sameOrigin()
            .and()
        .authorizeRequests()

https://docs.spring.io/spring-security/site/docs/current/reference/html/websocket.html

4
Edwin Diaz-Mendez

Pour rassembler toutes les choses au même endroit, j'écris ce post.

Lecture utile: websocket-authentication

WebSockets réutilise les mêmes informations d'authentification que celles trouvées dans La requête HTTP lors de l'établissement de la connexion WebSocket. Cela signifie Que le principal de la requête HttpServletRequest sera transféré à WebSockets. Si vous utilisez Spring Security, le principal de HttpServletRequest est automatiquement remplacé.

Plus concrètement, pour s'assurer qu'un utilisateur est authentifié auprès de votre application WebSocket , Il vous suffit de configurer Spring Security pour authentifier votre application Web basée sur HTTP.

Il faut donc avant tout activer la sécurité Web printanière habituelle. Par exemple comme ceci:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String SECURE_ADMIN_PASSWORD = "rockandroll";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .formLogin()
                .loginPage("/index.html")
                    .loginProcessingUrl("/login")
                    .defaultSuccessUrl("/sender.html")
                    .permitAll()
                .and()
                .logout()
                    .logoutSuccessUrl("/index.html")
                    .permitAll()
                .and()
                .authorizeRequests()
                .antMatchers("/js/**", "/lib/**", "/images/**", "/css/**", "/index.html", "/","/*.css","/webjars/**", "/*.js").permitAll()
                .antMatchers("/websocket").hasRole("ADMIN")
                .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
                .anyRequest().authenticated();

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

        auth.authenticationProvider(new AuthenticationProvider() {

            @Override
            public boolean supports(Class<?> authentication) {
                return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
            }

            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;

                List<GrantedAuthority> authorities = SECURE_ADMIN_PASSWORD.equals(token.getCredentials()) ?
                        AuthorityUtils.createAuthorityList("ROLE_ADMIN") : null;

                return new UsernamePasswordAuthenticationToken(token.getName(), token.getCredentials(), authorities);
            }
        });
    }
}

Et si vous avez correctement configuré la configuration de socket Web, vous devez ajouter une configuration de sécurité de socket Web qui étend la classe AbstractSecurityWebSocketMessageBrokerConfigurer comme ceci:

@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    @Override
    protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
        // You can customize your authorization mapping here.
        messages.anyMessage().authenticated();
        messages.simpDestMatchers("/app/hello").authenticated()//.hasRole("ADMIN")
                .simpSubscribeDestMatchers("/user/queue/**").hasRole("ADMIN")
                .simpSubscribeDestMatchers("/topic/greetings").authenticated();
    }

    // TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}

C'est comme ça. Ça marche.

Vous trouverez plus d’informations sur la configuration des autorisations de sécurité websocket ici: autorisation de sécurité des prises Web

1
gstackoverflow