最新の安定バージョンについては、Spring Security 6.4.2 を使用してください! |
WebSocket セキュリティ
Spring Security 4 は、Spring の WebSocket サポートを保護するためのサポートを追加しました。このセクションでは、Spring Security の WebSocket サポートの使用方法について説明します。
WebSocket 認証
WebSockets は、WebSocket 接続が確立されたときに HTTP リクエストで見つかった同じ認証情報を再利用します。これは、HttpServletRequest
上の Principal
が WebSockets に引き渡されることを意味します。Spring Security を使用している場合、HttpServletRequest
の Principal
は自動的にオーバーライドされます。
より具体的には、ユーザーが WebSocket アプリケーションに対して認証されたことを確認するには、HTTP ベースの Web アプリケーションを認証するように Spring Security をセットアップすることだけが必要です。
WebSocket 認証
Spring Security 4.0 は、Spring メッセージングの抽象化を通じて WebSockets の認可サポートを導入しました。
Spring Security 5.8 では、このサポートがリフレッシュされ、AuthorizationManager
API を使用できるようになりました。
Java 構成を使用して認可を構成するには、@EnableWebSocketSecurity
アノテーションを含めて AuthorizationManager<Message<?>>
Bean を発行するか、XML で use-authorization-manager
属性を使用します。これを行う 1 つの方法は、AuthorizationManagerMessageMatcherRegistry
を使用して次のようにエンドポイントパターンを指定することです。
Java
Kotlin
XML
@Configuration
@EnableWebSocketSecurity (1) (2)
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/user/**").hasRole("USER") (3)
return messages.build();
}
}
@Configuration
@EnableWebSocketSecurity (1) (2)
open class WebSocketSecurityConfig { (1) (2)
@Bean
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<*>> {
messages.simpDestMatchers("/user/**").hasRole("USER") (3)
return messages.build()
}
}
<websocket-message-broker use-authorization-manager="true"> (1) (2)
<intercept-message pattern="/user/**" access="hasRole('USER')"/> (3)
</websocket-message-broker>
1 | 受信 CONNECT メッセージには、同一生成元ポリシーを適用するための有効な CSRF トークンが必要です。 |
2 | SecurityContextHolder には、受信リクエストの simpUser ヘッダー属性内のユーザーが入力されます。 |
3 | メッセージには適切な認可が必要です。具体的には、/user/ で始まる受信メッセージには ROLE_USER が必要です。認可に関する追加の詳細については、WebSocket 認証を参照してください。 |
カスタム認証
AuthorizationManager
を使用する場合、カスタマイズは非常に簡単です。例: 以下に示すように、AuthorityAuthorizationManager
を使用して、すべてのメッセージが "USER" のロールを持つことを要求する AuthorizationManager
を公開できます。
Java
Kotlin
XML
@Configuration
@EnableWebSocketSecurity (1) (2)
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
return AuthorityAuthorizationManager.hasRole("USER");
}
}
@Configuration
@EnableWebSocketSecurity (1) (2)
open class WebSocketSecurityConfig {
@Bean
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<*>> {
return AuthorityAuthorizationManager.hasRole("USER") (3)
}
}
<bean id="authorizationManager" class="org.example.MyAuthorizationManager"/>
<websocket-message-broker authorization-manager-ref="myAuthorizationManager"/>
以下のより高度な例に見られるように、メッセージをさらに照合する方法はいくつかあります。
Java
Kotlin
XML
@Configuration
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder 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)
return messages.build();
}
}
@Configuration
open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<*>> {
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)
return messages.build();
}
}
<websocket-message-broker use-authorization-manager="true">
(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/**" type="SUBSCRIBE" access="hasRole('USER')" />
<intercept-message pattern="/topic/friends/*" type="SUBSCRIBE" 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 | 宛先のないメッセージ(つまり、メッセージ型が MESSAGE または SUBSCRIBE 以外のもの)では、ユーザーの認証が必要になります |
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
への通知を受信できます。
クライアントが SUBSCRIBE
から /topic/system/notifications
に送信できるようにしたいのですが、クライアントが MESSAGE
をその宛先に送信できるようにしたくはありません。MESSAGE
を /topic/system/notifications
に送信することを許可した場合、クライアントはそのエンドポイントに直接メッセージを送信し、システムになりすますことができます。
一般に、アプリケーションは、ブローカープレフィックス(/topic/
または /queue/
)で始まる宛先に送信された MESSAGE
を拒否するのが一般的です。
宛先での 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 Framework リファレンスドキュメントには、メッセージがシステムをどのように流れるかを説明する “メッセージの流れ” というタイトルのセクションが含まれています。Spring Security は clientInboundChannel
のみを保護することに注意してください。Spring Security は clientOutboundChannel
を保護しようとしません。
これの最も重要な理由はパフォーマンスです。受信するすべてのメッセージに対して、通常はさらに多くのメッセージが発信されます。送信メッセージを保護する代わりに、エンドポイントへのサブスクリプションを保護することをお勧めします。
同一生成元ポリシーの強制
ブラウザーは WebSocket 接続に対して同一生成元ポリシー [Wikipedia] を強制しないことに注意してください。これは非常に重要な考慮事項です。
なぜ同一生成元なのでしょうか?
次のシナリオを考えてみましょう。ユーザーが bank.com
にアクセスし、自分のアカウントに認証します。同じユーザーがブラウザーで別のタブを開き、evil.com
にアクセスします。同一生成元ポリシーにより、evil.com
は bank.com
からのデータの読み取りや bank.com
へのデータの書き込みを行うことができなくなります。
WebSockets では、同一生成元ポリシーは適用されません。実際、bank.com
が明示的に禁止しない限り、evil.com
はユーザーに代わってデータを読み書きできます。つまり、ユーザーが webSocket 経由で実行できる操作 (送金など) はすべて、evil.com
がそのユーザーに代わって実行できるということです。
SockJS は WebSockets をエミュレートしようとするため、同一生成元ポリシーもバイパスします。つまり、開発者は 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
を公開できます。例: 次の場合、/csrf
URL で 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 を無効にします
現時点では、@EnableWebSocketSecurity を使用する場合、CSRF は構成できませんが、これは将来のリリースで追加される可能性があります。 |
CSRF を無効にするには、@EnableWebSocketSecurity
を使用する代わりに、XML サポートを使用するか、次のように Spring Security コンポーネントを自分で追加できます。
Java
Kotlin
XML
@Configuration
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
AuthorizationManager<Message<?>> myAuthorizationRules = AuthenticatedAuthorizationManager.authenticated();
AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(myAuthorizationRules);
AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(this.context);
authz.setAuthorizationEventPublisher(publisher);
registration.interceptors(new SecurityContextChannelInterceptor(), authz);
}
}
@Configuration
open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer {
@Override
override fun addArgumentResolvers(argumentResolvers: List<HandlerMethodArgumentResolver>) {
argumentResolvers.add(AuthenticationPrincipalArgumentResolver())
}
@Override
override fun configureClientInboundChannel(registration: ChannelRegistration) {
var myAuthorizationRules: AuthorizationManager<Message<*>> = AuthenticatedAuthorizationManager.authenticated()
var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(myAuthorizationRules)
var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(this.context)
authz.setAuthorizationEventPublisher(publisher)
registration.interceptors(SecurityContextChannelInterceptor(), authz)
}
}
<websocket-message-broker use-authorization-manager="true" same-origin-disabled="true">
<intercept-message pattern="/**" access="authenticated"/>
</websocket-message-broker>
一方、従来の AbstractSecurityWebSocketMessageBrokerConfigurer
を使用していて、他のドメインによるサイトへのアクセスを許可したい場合は、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
}
}
カスタム式ハンドラー
場合によっては、intercept-message
XML 要素で定義された access
式の処理方法をカスタマイズすることに価値がある場合があります。これを行うには、型 SecurityExpressionHandler<MessageAuthorizationContext<?>>
のクラスを作成し、次のように XML 定義でそれを参照します。
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler"/>
SecurityExpressionHandler<Message<?>>
を実装する websocket-message-broker
の従来の使用箇所から移行する場合は、次のことができます。1. さらに createEvaluationContext(Supplier, Message)
メソッドを実装し、次に 2. その値を次のように MessageAuthorizationContextSecurityExpressionHandler
でラップします。
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler">
<b:constructor-arg>
<b:bean class="org.example.MyLegacyExpressionHandler"/>
</b:constructor-arg>
</b:bean>
SockJS での作業
SockJS は、古いブラウザーをサポートするためのフォールバックトランスポートを提供します。フォールバックオプションを使用する場合、SockJS が Spring Security で動作できるように、いくつかのセキュリティ制約を緩和する必要があります。
SockJS とフレームオプション
SockJS は、iframe [GitHub] (英語) を利用するトランスポートを使用する場合があります。デフォルトでは、Spring Security は、クリックジャッキング攻撃を防ぐためにサイトがフレーム化されることを拒否します。 SockJS フレームベースのトランスポートを機能させるには、同じオリジンがコンテンツをフレーム化するように Spring Security を構成する必要があります。
frame-options 要素を使用して X-Frame-Options
をカスタマイズできます。例: 以下は、Spring Security に X-Frame-Options: SAMEORIGIN
を使用するように指示します。これにより、同じドメイン内で iframe が許可されます。
<http>
<!-- ... -->
<headers>
<frame-options
policy="SAMEORIGIN" />
</headers>
</http>
同様に、以下を使用して、Java 構成内で同じオリジンを使用するようにフレームオプションをカスタマイズできます。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
);
return http.build();
}
}
@Configuration
@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
.ignoringRequestMatchers("/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 {
ignoringRequestMatchers("/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>
レガシー WebSocket 構成
Spring Security 5.8 以前は、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 トークンが必要です |
2 | SecurityContextHolder には、すべての受信リクエストの simpUser ヘッダー属性内のユーザーが入力されます。 |
3 | メッセージには適切な認可が必要です。具体的には、"/user/" で始まる受信メッセージには ROLE_USER が必要です。認可の詳細については、WebSocket 認証を参照してください。 |
AbstractSecurityExpressionHandler
を継承し、createEvaluationContextInternal
または createSecurityExpressionRoot
をオーバーライドするカスタム SecurityExpressionHandler
がある場合は、レガシー構成を使用すると便利です。Authorization
ルックアップを延期するために、新しい AuthorizationManager
API は、式を評価するときにこれらを呼び出しません。
XML を使用している場合は、use-authorization-manager
要素を使用しないか false
に設定するだけで、レガシー API を使用できます。