如何保护Websocket应用程序 [Spring Boot + STOMP]的安全性

8

早上好,

我创建了一个简单的Spring Boot WebSocket应用程序。现在我想为它设置一些安全性。我尝试了一些例子,但是我无法使它正常工作。我得到了以下错误:

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

登录STS:

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

堆栈跟踪:


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

我的配置文件:

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


}
}

控制器功能

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

连接的JavaScript函数:

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

我的问题是:网络浏览器正在连接应用程序并发送消息(我不知道它是否安全),但它无法从应用程序接收任何消息。
我的问题是:如何实现安全的WebSocket连接,以及如何消除该错误。
我对保护Web应用程序还很新,所以请多包涵。
谢谢您的建议。
安德鲁

你解决了吗?我也遇到了同样的问题。下面的答案和Spring.io的教程一点也不清楚。当你把一个字面字符串赋值给一个变量时,当然会得到bad token ${_csrf.headerName}:${_csrf.token}。而且它看起来不像JSTL的语法,而是JavaScript的语法。 - user3758745
2个回答

9

根据文档:

通常我们需要在HTTP标头或HTTP参数中包含CSRF令牌。然而,SockJS不允许这些选项。相反,我们必须将令牌包含在Stomp标头中。

好的,现在我们知道我们必须将这些标头作为Stomp标头包含。如果您在应用程序中使用JSP,则可以从客户端侧的请求属性中获取CSRF标头和令牌。

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

如果您没有使用JSP并且正在使用普通HTML,则必须在REST端点上公开CsrfToken令牌,例如在/csrf处:

@RestController
public class CsrfController {

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

还有一些注意事项。例如,假设您希望允许其他域访问您的Web套接字端点,在您的WebSocketSecurityConfig中,您可以提供以下内容:

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

我从文档中学到的最后一个重要的事情是:
SockJS在任何基于HTTP的传输上使用CONNECT消息的POST。通常,我们需要在HTTP头或HTTP参数中包含CSRF令牌。然而,SockJS不允许这些选项。相反,我们必须像第24.4.3节“将CSRF添加到Stomp标头”中描述的那样,在Stomp标头中包含令牌。
这也意味着我们需要放宽Web层的CSRF保护。具体来说,我们想要禁用我们的连接URL的CSRF保护。我们不想为每个URL禁用CSRF保护。否则,我们的网站将容易受到CSRF攻击。
这里的关键是第二段话,简单来说,您需要将以下内容添加到扩展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


1
这句话的意思不是很清楚,您能提供一个完整的例子吗? - gstackoverflow
如果应用程序是SPA,我认为使用CookieCsrfTokenRepository.withHttpOnlyFalse()更好。您可以从cookie获取csrf令牌:https://dev59.com/a1IG5IYBdhLWcg3w2VXM - lechat

1
为了将所有内容收集到一个地方,我写了这篇文章。
有用的阅读材料:websocket-authentication WebSockets重用在建立WebSocket连接时发现的HTTP请求中的身份验证信息。这意味着HttpServletRequest上的Principal将被移交给WebSockets。如果您使用Spring Security,则会自动覆盖HttpServletRequest上的Principal。
更具体地说,要确保用户已经对您的WebSocket应用程序进行了身份验证,您需要确保设置Spring Security来验证基于HTTP的Web应用程序。
因此,首先您需要启用普通的spring web安全。例如,像这样:
@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);
            }
        });
    }
}

如果您已经正确配置了Web Socket配置,则需要添加Web Socket安全配置,扩展类AbstractSecurityWebSocketMessageBrokerConfigurer,如下所示:

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

明白了,它可以运行。

您可以在此处找到有关WebSocket安全授权配置的更多信息:web socket security authorization


网址出问题了,我的朋友。 - Aliy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接