おはようございます、
簡単な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アプリケーションのセキュリティ保護は初めてですので、ご容赦ください。
アドバイスありがとうございます。
アンドリュー
ドキュメントから:
通常、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
すべてのものを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ソケットのセキュリティ認証