# WebSocket 安全

Spring Security4增加了对保护Spring’s WebSocket support (opens new window)的支持。本节介绍如何使用 Spring Security的 WebSocket 支持。

直接支持JSR-356

Spring 安全性不提供直接的JSR-356支持,因为这样做将提供很少的价值。这是因为格式未知,所以有little Spring can do to secure an unknown format (opens new window)。此外,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令牌来执行同源政策
2 对于任何入站请求,SecurityContextholder都在Simpuser header属性中填充用户。
3 我们的信息需要得到适当的授权。具体地说,任何以“/user/”开头的入站消息都需要角色_user。有关授权的更多详细信息,请参见WebSocket 授权

Spring 安全性还提供了XML命名空间用于保护WebSockets的支持。类似的基于XML的配置如下所示:

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

这将确保:

1 任何入站连接消息都需要一个有效的CSRF令牌来执行同源政策
2 对于任何入站请求,SecurityContextholder都在Simpuser header属性中填充用户。
3 我们的信息需要得到适当的授权。具体地说,任何以“/user/”开头的入站消息都需要角色_user。有关授权的更多详细信息,请参见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命名空间用于保护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”发送消息,那么客户端可以直接向该端点发送消息并模拟系统。

通常,应用程序会拒绝发送到以代理前缀 (opens new window)(即“/topic/”或“/queue/”)开头的目标的任何消息。

# WebSocket 目的地授权

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

考虑一个聊天应用程序。

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

  • 应用程序看到消息,确保将“from”属性指定为当前用户(我们不能信任客户端)。

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

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

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

通常,应用程序会拒绝发送到以代理前缀 (opens new window)(即“/topic/”或“/queue/”)开头的消息的任何订阅。当然,我们可能会提供例外情况,以解释诸如

# 出站消息

Spring 包含一个标题为消息流 (opens new window)的部分,该部分描述了消息如何在系统中流动。需要注意的是, Spring 安全性仅保护clientInboundChannel。 Spring 安全性不试图保护clientOutboundChannel

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

# 执行同源政策

需要强调的是,对于 WebSocket 连接,浏览器并不强制执行同源政策 (opens new window)。这是一个极其重要的考虑因素。

# 为什么是同源?

考虑以下场景。用户访问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支持限制了对当前域 (opens new window)的访问。 Spring 安全性增加了额外的保护层,以提供[纵深防御](https://en.wikipedia.org/wiki/Defense_in_depth_(computing))。

# 将CSRF添加到Stomp头

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

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

应用程序可以通过访问名为_CSRF的请求属性获取CSRF令牌。例如,下面将允许访问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 (opens new window)提供后备传输以支持较旧的浏览器。在使用后备选项时,我们需要放松一些安全约束,以允许Sockjs使用 Spring 安全性。

# sockjs&frame-选项

Sockjs可以使用利用iframe的传输 (opens new window)。 Spring 默认情况下,安全性将deny网站框起来,以防止点击劫持攻击。为了允许基于SockJS帧的传输工作,我们需要配置 Spring 安全性,以允许相同的源来帧内容。

你可以使用框架-选项元素自定义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头中描述的那样,在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 MVCSpring’s CORS Support