简体   繁体   English

如何保护websocket应用程序[Spring boot + STOMP]

[英]How to secure websocket application [Spring boot + STOMP]

Good morning, 早上好,

I've created a simple Spring Boot WebSocket application. 我创建了一个简单的Spring Boot WebSocket应用程序。 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: 登录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 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: JS函数连接:

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. 所以我的问题是:Web浏览器连接到应用程序并发送消息(我不知道它是否安全)但它无法从应用程序接收任何消息。

My question: How to achive secured websocket connection, and how to get rid of that error. 我的问题:如何实现安全的websocket连接,以及如何摆脱该错误。

I'm new to securing web application so please be forgiving. 我是新手保护网络应用程序所以请原谅。

Thank You for any advice. 谢谢你的任何建议。

Andrew 安德鲁

From the documentation: 从文档:

Typically we need to include the CSRF token in an HTTP header or an HTTP parameter. 通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。 However, SockJS does not allow for these options. 但是,SockJS不允许这些选项。 Instead, we must include the token in the Stomp headers 相反,我们必须在Stomp标头中包含令牌

Ok we now know that we have to include these headers as Stomp headers. 好的,我们现在知道我们必须将这些标头包含为Stomp标头。 If you are using JSPs in your application, you can obtain the CSRF headers and tokens from the request attributes on the client side. 如果在应用程序中使用JSP,则可以从客户端的请求属性中获取CSRF标头和标记。

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, eg at /csrf : 如果您使用JSP并使用普通HTML,则必须在REST端点上公开CsrfToken令牌,例如在/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: 例如,假设您希望允许其他域访问您的Web套接字端点,您可以在WebSocketSecurityConfig中提供以下内容:

@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. 对于任何基于HTTP的传输,SockJS在CONNECT消息上使用POST。 Typically we need to include the CSRF token in an HTTP header or an HTTP parameter. 通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。 However, SockJS does not allow for these options. 但是,SockJS不允许这些选项。 Instead, we must include the token in the Stomp headers as described in Section 24.4.3, “Adding CSRF to Stomp Headers”. 相反,我们必须在第24.4.3节“将CSRF添加到Stomp Headers”中所述的Stomp标头中包含该标记。

It also means we need to relax our CSRF protection with the web layer. 这也意味着我们需要通过Web层放松我们的CSRF保护。 Specifically, we want to disable CSRF protection for our connect URLs. 具体来说,我们要为连接URL禁用CSRF保护。 We do NOT want to disable CSRF protection for every URL. 我们不想为每个URL禁用CSRF保护。 Otherwise our site will be vulnerable to CSRF attacks. 否则我们的网站将容易受到CSRF攻击。

The second paragraph is the key here, in simpler terms, you have to add the following to the class that extended WebSecurityConfigurerAdapter 第二段是这里的关键,简单来说,你必须将以下内容添加到扩展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 https://docs.spring.io/spring-security/site/docs/current/reference/html/websocket.html

To collect all stuff in one place I am writing this post. 要在一个地方收集所有东西,我正在写这篇文章。

Helpful reading: websocket-authentication 有用的阅读: websocket-authentication

WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made. WebSockets重用与WebSocket连接时在HTTP请求中找到的相同身份验证信息。 This means that the Principal on the HttpServletRequest will be handed off to WebSockets. 这意味着HttpServletRequest上的Principal将被移交给WebSockets。 If you are using Spring Security, the Principal on the HttpServletRequest is overridden automatically. 如果您使用的是Spring Security,则会自动覆盖HttpServletRequest上的Principal。

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. 更具体地说,为确保用户已对WebSocket应用程序进行身份验证,所有必要的是确保您设置Spring Security以对基于HTTP的Web应用程序进行身份验证。

So first of all you need to enable usual spring web security. 首先,您需要启用通常的spring web安全性。 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: 如果您已正确配置Web套接字配置,则需要添加扩展AbstractSecurityWebSocketMessageBrokerConfigurer类的Web套接字安全配置,如下所示:

@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 您可以在此处找到有关websocket安全授权配置的更多信息: Web套接字安全授权

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM