web-dev-qa-db-ja.com

WebSocketアプリケーションを保護する方法[Springboot + STOMP]

おはようございます、

簡単なSpringBootWebSocketアプリケーションを作成しました。今、私はそれにいくつかのセキュリティを設定したいと思います。私はいくつかの例を試しましたが、うまくいきません。エラーが発生します:

ウェブブラウザ:

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

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

私の設定ファイル:

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

接続する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);
    });
});
}

だから私の問題は:ウェブブラウザはアプリケーションに接続してメッセージを送信していますが(セキュリティで保護されているかどうかはわかりません)、アプリケーションからメッセージを受信できません。

私の質問:安全なWebSocket接続を実現する方法と、そのエラーを取り除く方法。

Webアプリケーションのセキュリティ保護は初めてですので、ご容赦ください。

アドバイスありがとうございます。

アンドリュー

5
Andrew

ドキュメントから:

通常、CSRFトークンをHTTPヘッダーまたはHTTPパラメーターに含める必要があります。ただし、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項「StompヘッダーへのCSRFの追加」で説明されているように、Stompヘッダーにトークンを含める必要があります。

また、Webレイヤーを使用してCSRF保護を緩和する必要があることも意味します。具体的には、接続URLのCSRF保護を無効にします。すべてのURLでCSRF保護を無効にする必要はありません。そうしないと、当サイトはCSRF攻撃に対して脆弱になります。

2番目の段落はここでの鍵です。簡単に言うと、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

6

すべてのものを1か所に集めるために、私はこの投稿を書いています。

参考資料: websocket-authentication

WebSocketは、WebSocket接続が確立されたときにHTTP要求で検出されたものと同じ認証情報を再利用します。これは、HttpServletRequestのプリンシパルがWebSocketに渡されることを意味します。 Spring Securityを使用している場合、HttpServletRequestのプリンシパルは自動的に上書きされます。

より具体的には、ユーザーがWebSocketアプリケーションに対して認証されていることを確認するために必要なのは、HTTPベースのWebアプリケーションを認証するようにSpringSecurityを設定することだけです。

したがって、まず最初に、通常のSpringWebセキュリティを有効にする必要があります。たとえば、次のようになります。

@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ソケット構成を適切に構成している場合は、次のようにクラス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;
    }
}

そうです。できます。

WebSocketのセキュリティ認証構成の詳細については、次を参照してください。 Webソケットのセキュリティ認証

1
gstackoverflow