最新の安定バージョンについては、Spring Security 6.3.1 を使用してください!

WebSocket セキュリティ

Spring Security 4 は、Spring の WebSocket サポートを保護するためのサポートを追加しました。このセクションでは、Spring Security の WebSocket サポートの使用方法について説明します。

JSR-356 の直接サポート

Spring Security は、JSR-356 の直接的なサポートを提供しません。なぜなら、そうするとほとんど価値がなくなるからです。これは、形式が不明であるため、不明な形式を保護するために Spring が実行できることはほとんどないためです。さらに、JSR-356 はメッセージをインターセプトする方法を提供しないため、セキュリティはかなり侵襲的です。

WebSocket の設定

Spring Security 4.0 は、Spring メッセージング抽象化による WebSockets の認可サポートを導入しました。Java 構成を使用して認証を設定するには、AbstractSecurityWebSocketMessageBrokerConfigurer を継承して MessageSecurityMetadataSourceRegistry を設定するだけです。例:

  • Java

  • Kotlin

@Configuration
public class WebSocketSecurityConfig
      extends AbstractSecurityWebSocketMessageBrokerConfigurer { (1) (2)

    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpDestMatchers("/user/**").authenticated() (3)
    }
}
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { (1) (2)
    override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
        messages.simpDestMatchers("/user/**").authenticated() (3)
    }
}

これにより、次のことが保証されます。

1 受信 CONNECT メッセージには、同一生成元ポリシーを実施するための有効な CSRF トークンが必要です
2SecurityContextHolder には、受信リクエストの simpUser ヘッダー属性内のユーザーが入力されます。
3 メッセージには適切な認可が必要です。具体的には、"/user/" で始まる受信メッセージには ROLE_USER が必要です。認可の詳細については、WebSocket 認証を参照してください。

Spring Security は、WebSockets を保護するための XML 名前空間サポートも提供します。比較可能な XML ベースの構成は次のようになります。

<websocket-message-broker> (1) (2)
    (3)
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
</websocket-message-broker>

これにより、次のことが保証されます。

1 受信 CONNECT メッセージには、同一生成元ポリシーを実施するための有効な CSRF トークンが必要です
2SecurityContextHolder には、受信リクエストの simpUser ヘッダー属性内のユーザーが入力されます。
3 メッセージには適切な認可が必要です。具体的には、"/user/" で始まる受信メッセージには ROLE_USER が必要です。認可の詳細については、WebSocket 認証を参照してください。

WebSocket 認証

WebSockets は、WebSocket 接続が確立されたときに HTTP リクエストで見つかった同じ認証情報を再利用します。これは、HttpServletRequest 上の Principal が WebSockets に引き渡されることを意味します。Spring Security を使用している場合、HttpServletRequest の Principal は自動的にオーバーライドされます。

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

WebSocket 認証

Spring Security 4.0 は、Spring メッセージング抽象化による WebSockets の認可サポートを導入しました。Java 構成を使用して認証を設定するには、AbstractSecurityWebSocketMessageBrokerConfigurer を継承して MessageSecurityMetadataSourceRegistry を設定するだけです。例:

  • Java

  • Kotlin

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .nullDestMatcher().authenticated() (1)
                .simpSubscribeDestMatchers("/user/queue/errors").permitAll() (2)
                .simpDestMatchers("/app/**").hasRole("USER") (3)
                .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") (4)
                .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() (5)
                .anyMessage().denyAll(); (6)

    }
}
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
    override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
        messages
            .nullDestMatcher().authenticated() (1)
            .simpSubscribeDestMatchers("/user/queue/errors").permitAll() (2)
            .simpDestMatchers("/app/**").hasRole("USER") (3)
            .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") (4)
            .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() (5)
            .anyMessage().denyAll() (6)
    }
}

これにより、次のことが保証されます。

1 宛先のないメッセージ(つまり、メッセージ型が MESSAGE または SUBSCRIBE 以外のもの)では、ユーザーの認証が必要になります
2 誰でも /user/queue/errors にサブスクライブできます
3"/app/" で始まる宛先を持つメッセージには、ユーザーが ROLE_USER のロールを持っている必要があります。
4 型 SUBSCRIBE の "/user/" または "/topic/friends/" で始まるメッセージには、ROLE_USER が必要です。
5 型 MESSAGE または SUBSCRIBE の他のメッセージは拒否されます。6 のため、この手順は必要ありませんが、特定のメッセージ型でどのように一致するかを示しています。
6 その他のメッセージは拒否されます。これは、メッセージを見逃さないようにするための良いアイデアです。

Spring Security は、WebSockets を保護するための XML 名前空間サポートも提供します。比較可能な XML ベースの構成は次のようになります。

<websocket-message-broker>
    (1)
    <intercept-message type="CONNECT" access="permitAll" />
    <intercept-message type="UNSUBSCRIBE" access="permitAll" />
    <intercept-message type="DISCONNECT" access="permitAll" />

    <intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> (2)
    <intercept-message pattern="/app/**" access="hasRole('USER')" />      (3)

    (4)
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
    <intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />

    (5)
    <intercept-message type="MESSAGE" access="denyAll" />
    <intercept-message type="SUBSCRIBE" access="denyAll" />

    <intercept-message pattern="/**" access="denyAll" /> (6)
</websocket-message-broker>

これにより、次のことが保証されます。

1 型 CONNECT、UNSUBSCRIBE、DISCONNECT のメッセージでは、ユーザーの認証が必要です。
2 誰でも /user/queue/errors にサブスクライブできます
3"/app/" で始まる宛先を持つメッセージには、ユーザーが ROLE_USER のロールを持っている必要があります。
4 型 SUBSCRIBE の "/user/" または "/topic/friends/" で始まるメッセージには、ROLE_USER が必要です。
5 型 MESSAGE または SUBSCRIBE の他のメッセージは拒否されます。6 のため、この手順は必要ありませんが、特定のメッセージ型でどのように一致するかを示しています。
6 宛先を持つ他のメッセージは拒否されます。これは、メッセージを見逃さないようにするための良いアイデアです。

WebSocket 認可に関する注意

アプリケーションを適切に保護するには、Spring の WebSocket サポートを理解することが重要です。

メッセージ型の WebSocket 認可

メッセージの SUBSCRIBE 型と MESSAGE 型の違いと、Spring 内での動作方法を理解することが重要です。

チャットアプリケーションを検討してください。

  • システムは、"/topic/system/notifications" の宛先を介してすべてのユーザーに通知 MESSAGE を送信できます。

  • クライアントは、SUBSCRIBE から "/topic/system/notifications" への通知を受け取ることができます。

クライアントが "/topic/system/notifications" にサブスクライブできるようにしたいのですが、クライアントがその宛先にメッセージを送信できるようにしたくはありません。MESSAGE を "/topic/system/notifications" に送信することを許可した場合、クライアントはそのエンドポイントに直接メッセージを送信し、システムになりすますことができます。

一般に、アプリケーションは、ブローカープレフィックス(つまり、"/topic/" または "/queue/")で始まる宛先に送信されたメッセージを拒否するのが一般的です。

宛先での WebSocket 認可

宛先がどのように変換されるかを理解することも重要です。

チャットアプリケーションを検討してください。

  • ユーザーは、"/app/chat" の宛先にメッセージを送信することにより、特定のユーザーにメッセージを送信できます。

  • アプリケーションはメッセージを見て、"from" 属性が現在のユーザーとして指定されていることを確認します(クライアントを信頼できません)。

  • 次に、アプリケーションは SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message) を使用して受信者にメッセージを送信します。

  • メッセージは "/queue/user/messages-<sessionid>" の宛先に変換されます

上記のアプリケーションでは、クライアントが "/queue/user/messages-<sessionid>" に変換される "/user/queue" をリッスンできるようにします。ただし、クライアントが "/queue/*" をリッスンできるようにする必要はありません。これにより、クライアントはすべてのユーザーのメッセージを表示できるようになります。

一般に、アプリケーションは、ブローカープレフィックス(つまり、"/topic/" または "/queue/")で始まるメッセージに送信された SUBSCRIBE を拒否するのが一般的です。もちろん、次のようなことを説明するために例外を提供する場合があります

送信メッセージ

Spring には、メッセージがシステムをどのように流れるかを説明するメッセージの流れというタイトルのセクションが含まれています。Spring Security は clientInboundChannel のみを保護することに注意することが重要です。Spring Security は clientOutboundChannel を保護しようとしません。

これの最も重要な理由はパフォーマンスです。受信するすべてのメッセージには、通常、さらに多くのメッセージが送信されます。発信メッセージを保護する代わりに、エンドポイントへのサブスクリプションを保護することをお勧めします。

同一生成元ポリシーの強制

ブラウザーが WebSocket 接続に対して同一生成元ポリシー [Wikipedia] を強制しないことを強調することが重要です。これは非常に重要な考慮事項です。

なぜ同一生成元なのでしょうか?

次のシナリオを検討してください。ユーザーが bank.com にアクセスして、アカウントの認証を行います。同じユーザーがブラウザーで別のタブを開き、evil.com にアクセスします。Same Origin Policy は、evil.com が bank.com に対してデータを読み書きできないようにします。

WebSockets では、同一生成元ポリシーは適用されません。実際、bank.com が明示的に禁止しない限り、evil.com はユーザーに代わってデータの読み取りと書き込みを行うことができます。つまり、ユーザーが webSocket でできること(つまり、送金)はすべて、evil.com がそのユーザーに代わって行うことができます。

SockJS は WebSockets をエミュレートしようとするため、Same Origin Policy もバイパスします。これは、SockJS を使用する場合、開発者がアプリケーションを外部ドメインから明示的に保護する必要があることを意味します。

Spring WebSocket 許可されたオリジン

幸い、Spring 4.1.5 の Spring WebSocket と SockJS のサポートにより、現在のドメインへのアクセスが制限されています。Spring Security は、多層防御 [Wikipedia] を提供するための保護層を追加します。

CSRF を Stomp ヘッダーに追加する

デフォルトでは、Spring Security は、すべての CONNECT メッセージ型で CSRF トークンを必要とします。これにより、CSRF トークンにアクセスできるサイトのみが接続できるようになります。同じ起源のみが CSRF トークンにアクセスできるため、外部ドメインは接続を確立できません。

通常、CSRF トークンを HTTP ヘッダーまたは HTTP パラメーターに含める必要があります。ただし、SockJS はこれらのオプションを許可しません。代わりに、トークンを Stomp ヘッダーに含める必要があります

アプリケーションは、_csrf という名前のリクエスト属性にアクセスすることで CSRF トークンを取得できます。例: 次の場合、JSP で CsrfToken にアクセスできます。

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

静的 HTML を使用している場合、REST エンドポイントで CsrfToken を公開できます。例: 以下は、URL/csrf で CsrfToken を公開します

  • Java

  • Kotlin

@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}
@RestController
class CsrfController {
    @RequestMapping("/csrf")
    fun csrf(token: CsrfToken): CsrfToken {
        return token
    }
}

JavaScript は、エンドポイントに対して REST 呼び出しを行い、レスポンスを使用して headerName とトークンを設定できます。

これで、トークンを Stomp クライアントに含めることができます。例:

...
var headers = {};
headers[headerName] = token;
stompClient.connect(headers, function(frame) {
  ...

}

WebSockets 内で CSRF を無効にします

他のドメインからサイトへのアクセスを許可する場合は、Spring Security の保護を無効にできます。例: Java 構成では、次を使用できます。

  • Java

  • Kotlin

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ...

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {

    // ...

    override fun sameOriginDisabled(): Boolean {
        return true
    }
}

SockJS での作業

SockJS は、古いブラウザーをサポートするフォールバックトランスポートを提供します。フォールバックオプションを使用する場合、SockJS が Spring Security と連携できるように、いくつかのセキュリティ制約を緩和する必要があります。

SockJS とフレームオプション

SockJS は、iframe を活用 [GitHub] (英語) するトランスポートを使用する場合があります。デフォルトでは、Spring Security は、クリックジャッキング攻撃を防ぐためにサイトがフレーム化されることを拒否します。 SockJS フレームベースのトランスポートが機能するようにするには、同じオリジンがコンテンツをフレーム化できるように Spring Security を構成する必要があります。

frame-options 要素を使用して X-Frame-Options をカスタマイズできます。例: 以下は、同じドメイン内で iframe を許可する "X-Frame-Options:SAMEORIGIN" を使用するように Spring Security に指示します。

<http>
    <!-- ... -->

    <headers>
        <frame-options
          policy="SAMEORIGIN" />
    </headers>
</http>

同様に、フレームオプションをカスタマイズして、次を使用して Java 構成内で同じオリジンを使用できます。

  • Java

  • Kotlin

@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions
                     .sameOrigin()
                )
        );
        return http.build();
    }
}
@EnableWebSecurity
open class WebSecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            headers {
                frameOptions {
                    sameOrigin = true
                }
            }
        }
        return http.build()
    }
}

SockJS とリラックスした CSRF

SockJS は、HTTP ベースのトランスポートの CONNECT メッセージで POST を使用します。通常、CSRF トークンを HTTP ヘッダーまたは HTTP パラメーターに含める必要があります。ただし、SockJS はこれらのオプションを許可しません。代わりに、CSRF を Stomp ヘッダーに追加するに従って、トークンを Stomp ヘッダーに含める必要があります。

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

CSRF RequestMatcher を提供することで、これを簡単に実現できます。Java 構成により、これは非常に簡単になります。例: ストンプエンドポイントが "/chat" の場合、次の構成を使用して、"/chat/" で始まる URL に対してのみ CSRF 保護を無効にできます。

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // ignore our stomp endpoints since they are protected using Stomp headers
                .ignoringAntMatchers("/chat/**")
            )
            .headers(headers -> headers
                // allow same origin to frame our site to support iframe SockJS
                .frameOptions(frameOptions -> frameOptions
                    .sameOrigin()
                )
            )
            .authorizeHttpRequests(authorize -> authorize
                ...
            )
            ...
@Configuration
@EnableWebSecurity
open class WebSecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            csrf {
                ignoringAntMatchers("/chat/**")
            }
            headers {
                frameOptions {
                    sameOrigin = true
                }
            }
            authorizeRequests {
                // ...
            }
            // ...

XML ベースの構成を使用している場合、csrf@request-matcher-ref を使用できます。例:

<http ...>
    <csrf request-matcher-ref="csrfMatcher"/>

    <headers>
        <frame-options policy="SAMEORIGIN"/>
    </headers>

    ...
</http>

<b:bean id="csrfMatcher"
    class="AndRequestMatcher">
    <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
    <b:constructor-arg>
        <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
          <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
            <b:constructor-arg value="/chat/**"/>
          </b:bean>
        </b:bean>
    </b:constructor-arg>
</b:bean>