How to secure websocket application [Spring boot + STOMP]

天涯浪子 提交于 2020-01-01 10:15:07

问题


Good morning,

I've created a simple Spring Boot WebSocket application. Right now I would like to set some security to it. I've trying some examples but I cannot get it worked. I'm getting error:

web browser:

>>> 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

Log in STS:

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

StackTrace:

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

My configuration files:

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


}
}

Controller function

@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());
}

JS function to connect:

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

So my problem is: web browser is connecting to application and send message (I dont know if it is secured) but it cannot receive any message from application.

My question: How to achive secured websocket connection, and how to get rid of that error.

I'm new to securing web application so please be forgiving.

Thank You for any advice.

Andrew


回答1:


From the documentation:

Typically we need to include the CSRF token in an HTTP header or an HTTP parameter. However, SockJS does not allow for these options. Instead, we must include the token in the Stomp headers

Ok we now know that we have to include these headers as Stomp headers. If you are using JSPs in your application, you can obtain the CSRF headers and tokens from the request attributes on the client side.

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

If you are not using JSPs and are using normal HTML, you have to expose the CsrfToken token on a REST endpoint, e.g. at /csrf:

@RestController
public class CsrfController {

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

There are further caveats. For example, let's say you want to allow other domains to access your web socket endpoint, in your WebSocketSecurityConfig you can provide the following:

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

One last important thing I learned is the following from the documentation:

SockJS uses a POST on the CONNECT messages for any HTTP based transport. Typically we need to include the CSRF token in an HTTP header or an HTTP parameter. However, SockJS does not allow for these options. Instead, we must include the token in the Stomp headers as described in Section 24.4.3, “Adding CSRF to Stomp Headers”.

It also means we need to relax our CSRF protection with the web layer. Specifically, we want to disable CSRF protection for our connect URLs. We do NOT want to disable CSRF protection for every URL. Otherwise our site will be vulnerable to CSRF attacks.

The second paragraph is the key here, in simpler terms, you have to add the following to the class that extended 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




回答2:


To collect all stuff in one place I am writing this post.

Helpful reading: websocket-authentication

WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made. This means that the Principal on the HttpServletRequest will be handed off to WebSockets. If you are using Spring Security, the Principal on the HttpServletRequest is overridden automatically.

More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application.

So first of all you need to enable usual spring web security. For example like this:

@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);
            }
        });
    }
}

And if you have properly configured web socket configuration you need to add web socket security configuration extending the class AbstractSecurityWebSocketMessageBrokerConfigurer like this:

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

That so. It works.

You can find more information about websocket security authorization configuration here:web socket security authorization



来源:https://stackoverflow.com/questions/48903044/how-to-secure-websocket-application-spring-boot-stomp

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!