# WebSocket 安全

Spring Security4增加了对保护[Spring’s WebSocket support](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html)的支持。本节介绍如何使用 Spring Security的 WebSocket 支持。

直接支持JSR-356

Spring 安全性不提供直接的JSR-356支持，因为这样做将提供很少的价值。这是因为格式未知，所以有[little Spring can do to secure an unknown format](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-intro-sub-protocol)。此外，JSR-356不提供截获消息的方法，因此安全性将是侵入性的。

## WebSocket 配置

Spring Security4.0通过 Spring 消息抽象引入了对WebSockets的授权支持。要使用 Java 配置配置来配置授权，只需扩展`AbstractSecurityWebSocketMessageBrokerConfigurer`并配置`MessageSecurityMetadataSourceRegistry`。例如：

Java

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

    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpDestMatchers("/user/**").authenticated() (3)
    }
}
```

Kotlin

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

这将确保：

|**1**|任何入站连接消息都需要一个有效的CSRF令牌来执行[同源政策](#websocket-sameorigin)|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|对于任何入站请求，SecurityContextholder都在Simpuser header属性中填充用户。|
|**3**|我们的信息需要得到适当的授权。具体地说，任何以“/user/”开头的入站消息都需要角色\_user。有关授权的更多详细信息，请参见[WebSocket 授权](#websocket-authorization)。|

Spring 安全性还提供了[XML命名空间](../appendix/namespace/websocket.html#nsa-websocket-security)用于保护WebSockets的支持。类似的基于XML的配置如下所示：

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

这将确保：

|**1**|任何入站连接消息都需要一个有效的CSRF令牌来执行[同源政策](#websocket-sameorigin)|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|对于任何入站请求，SecurityContextholder都在Simpuser header属性中填充用户。|
|**3**|我们的信息需要得到适当的授权。具体地说，任何以“/user/”开头的入站消息都需要角色\_user。有关授权的更多详细信息，请参见[WebSocket Authorization](#websocket-authorization)。|

## WebSocket 认证

WebSockets重用在建立 WebSocket 连接时在HTTP请求中找到的相同的身份验证信息。这意味着`Principal`上的`HttpServletRequest`将被传递给WebSockets。如果使用 Spring 安全性，则`HttpServletRequest`上的`Principal`将自动被重写。

更具体地说，为了确保用户已经对你的 WebSocket 应用程序进行了身份验证，所有必要的是确保设置 Spring 安全性以对基于HTTP的Web应用程序进行身份验证。

## WebSocket Authorization

Spring Security4.0通过 Spring 消息抽象引入了对WebSockets的授权支持。要使用 Java 配置配置来配置授权，只需扩展`AbstractSecurityWebSocketMessageBrokerConfigurer`并配置`MessageSecurityMetadataSourceRegistry`。例如：

Java

```
@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)

    }
}
```

Kotlin

```
@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**|任何没有目的地的消息（即消息类型或订阅以外的任何消息）都需要对用户进行身份验证。|
|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|任何人都可以订阅/user/queue/errors|
|**3**|任何具有以“/app/”开头的目标的消息都将要求用户具有角色\_user|
|**4**|任何以“/user/”或“/topic/friends/”开头、类型为Subscribe的消息都需要角色\_user|
|**5**|任何其他类型为消息或订阅的消息都将被拒绝。由于6，我们不需要这个步骤，但它说明了如何在特定的消息类型上进行匹配。|
|**6**|任何其他消息都将被拒绝。这是一个好主意，以确保你不会错过任何消息。|

Spring 安全性还提供了[XML命名空间](../appendix/namespace/websocket.html#nsa-websocket-security)用于保护WebSockets的支持。类似的基于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**|任何类型为“连接”、“取消订阅”或“断开连接”的消息都需要对用户进行身份验证。|
|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|任何人都可以订阅/user/queue/errors|
|**3**|任何具有以“/app/”开头的目标的消息都将要求用户具有角色\_user|
|**4**|任何以“/user/”或“/topic/friends/”开头、类型为Subscribe的消息都需要角色\_user|
|**5**|任何其他类型为消息或订阅的消息都将被拒绝。由于6，我们不需要这个步骤，但它说明了如何在特定的消息类型上进行匹配。|
|**6**|具有目的的任何其他消息都将被拒绝。这是一个好主意，以确保你不会错过任何消息。|

### WebSocket 授权说明

为了正确地保护你的应用程序，理解 Spring 的 WebSocket 支持非常重要。

#### WebSocket 对消息类型的授权

理解消息的订阅和消息类型之间的区别以及它在 Spring 中的工作方式非常重要。

考虑一个聊天应用程序。

* 系统可以通过目标“/topic/system/notifications”向所有用户发送通知消息。

* 客户端可以通过订阅“/topic/system/notification”来接收通知。

虽然我们希望客户机能够订阅“/topic/system/notifications”，但我们并不希望使他们能够向该目的地发送消息。如果我们允许向“/topic/system/notifications”发送消息，那么客户端可以直接向该端点发送消息并模拟系统。

通常，应用程序会拒绝发送到以[代理前缀](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp)（即“/topic/”或“/queue/”）开头的目标的任何消息。

#### WebSocket 目的地授权

了解目的地是如何转变的也很重要。

考虑一个聊天应用程序。

* 用户可以通过向“/app/chat”的目的地发送消息来向特定用户发送消息。

* 应用程序看到消息，确保将“from”属性指定为当前用户（我们不能信任客户端）。

* 然后，应用程序使用`SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)`将消息发送给收件人。

* 消息被转换为“/queue/user/messages-\<sessionid\>”的目标。

对于上面的应用程序，我们希望允许我们的客户机侦听“/user/queue”，它被转换为“/queue/user/messages-\<sessionid\>”。但是，我们不希望客户机能够侦听“/queue/\*”，因为这将允许客户机查看每个用户的消息。

通常，应用程序会拒绝发送到以[代理前缀](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp)（即“/topic/”或“/queue/”）开头的消息的任何订阅。当然，我们可能会提供例外情况，以解释诸如

### 出站消息

Spring 包含一个标题为[消息流](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow)的部分，该部分描述了消息如何在系统中流动。需要注意的是， Spring 安全性仅保护`clientInboundChannel`。 Spring 安全性不试图保护`clientOutboundChannel`。

这其中最重要的原因是业绩。对于每一条传入的消息，通常都会有更多的消息传出。我们鼓励保护对端点的订阅，而不是保护出站消息。

## 执行同源政策

需要强调的是，对于 WebSocket 连接，浏览器并不强制执行[同源政策](https://en.wikipedia.org/wiki/Same-origin_policy)。这是一个极其重要的考虑因素。

### 为什么是同源？

考虑以下场景。用户访问bank.com并对其帐户进行身份验证。同一个用户在浏览器中打开另一个标签，然后访问Evil.com。相同的源策略确保Evil.com不能将数据读写到bank.com。

对于WebSockets，相同的源策略不适用。事实上，除非bank.com明确禁止，否则evil.com可以代表用户读写数据。这意味着用户可以在 WebSocket 上做的任何事情（即转账），Evil.com都可以代表用户做。

由于Sockjs试图模拟WebSockets，因此它也绕过了相同的源策略。这意味着开发人员在使用Sockjs时需要显式地保护其应用程序不受外部域的影响。

### Spring  WebSocket 允许原产地

幸运的是，由于 Spring 4.1.5 Spring 的 WebSocket 和Sockjs支持限制了对[当前域](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-allowed-origins)的访问。 Spring 安全性增加了额外的保护层，以提供[纵深防御](https://en.wikipedia.org/wiki/Defense_in_depth_(computing)）。

### 将CSRF添加到Stomp头

Spring 默认情况下，安全性要求在任何连接消息类型中使用[CSRF token](../../features/exploits/csrf.html#csrf)。这确保只有能够访问CSRF令牌的站点才能进行连接。由于只有**同源**可以访问CSRF令牌，因此不允许外部域进行连接。

通常，我们需要在HTTP报头或HTTP参数中包含CSRF令牌。然而，Sockjs不允许这些选项。相反，我们必须在Stomp头中包含令牌。

应用程序可以通过访问名为\_CSRF的请求属性[获取CSRF令牌](../exploits/csrf.html#servlet-csrf-include)。例如，下面将允许访问JSP中的`CsrfToken`:

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

如果使用静态HTML，则可以在REST端点上公开`CsrfToken`。例如，下面将公开URL/CSRF上的`CsrfToken`

Java

```
@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}
```

Kotlin

```
@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

```
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ...

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}
```

Kotlin

```
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {

    // ...

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

## 与Sockjs合作

[SockJS](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-fallback)提供后备传输以支持较旧的浏览器。在使用后备选项时，我们需要放松一些安全约束，以允许Sockjs使用 Spring 安全性。

### sockjs&frame-选项

Sockjs可以使用[利用iframe的传输](https://github.com/sockjs/sockjs-client/tree/v0.3.4)。 Spring 默认情况下，安全性将[deny](../../features/exploits/headers.html#headers-frame-options)网站框起来，以防止点击劫持攻击。为了允许基于SockJS帧的传输工作，我们需要配置 Spring 安全性，以允许相同的源来帧内容。

你可以使用[框架-选项](../appendix/namespace/http.html#nsa-frame-options)元素自定义X-frame-options。例如，下面将指示 Spring Security使用“X-Frame-Options:SameOrigin”，它允许在相同的域内使用IFrames:

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

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

类似地，你可以使用以下方式自定义框架选项，以便在 Java 配置中使用相同的原点：

Java

```
@EnableWebSecurity
public class WebSecurityConfig extends
   WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions
                     .sameOrigin()
                )
        );
    }
}
```

Kotlin

```
@EnableWebSecurity
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            // ...
            headers {
                frameOptions {
                    sameOrigin = true
                }
            }
        }
    }
}
```

### Sockjs&放松的CSRF

对于任何基于HTTP的传输，Sockjs都会在Connect消息上使用POST。通常，我们需要在HTTP报头或HTTP参数中包含CSRF令牌。然而，Sockjs不允许这些选项。相反，我们必须像[将CSRF添加到Stomp头](#websocket-sameorigin-csrf)中描述的那样，在stomp头中包含令牌。

这也意味着我们需要放松对Web层的CSRF保护。具体地说，我们希望禁用我们的连接URL的CSRF保护。我们不希望禁用每个URL的CSRF保护。否则，我们的网站将容易受到CSRF攻击。

我们可以通过提供CSRF请求匹配器轻松地实现这一点。我们的 Java 配置使这一点变得非常容易。例如，如果我们的Stomp端点是“/chat”，那么我们可以使用以下配置，仅禁用以“/chat/”开头的URL的CSRF保护：

Java

```
@Configuration
@EnableWebSecurity
public class WebSecurityConfig
    extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(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
                ...
            )
            ...
```

Kotlin

```
@Configuration
@EnableWebSecurity
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            csrf {
                ignoringAntMatchers("/chat/**")
            }
            headers {
                frameOptions {
                    sameOrigin = true
                }
            }
            authorizeRequests {
                // ...
            }
            // ...
```

如果我们使用基于XML的配置，我们可以使用[[电子邮件保护]]（../acception/namespace/http.html#NSA-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>
```

[Spring MVC](mvc.html)[Spring’s CORS Support](cors.html)