23. WebSocket Security

Spring Security 4 增加了对保护Spring 的 WebSocket 支持的支持。本节描述了如何使用 Spring Security 的 WebSocket 支持。

Note

您可以在 samples/javaconfig/chat 中找到 WebSocket 安全性的完整工作示例。

Direct JSR-356 Support

Spring Security 不提供直接的 JSR-356 支持,因为这样做几乎没有价值。这是因为格式未知,所以有Spring 可以做些什么来保护未知格式。另外,JSR-356 没有提供拦截消息的方法,因此安全性将具有相当大的侵入性。

23.1 WebSocket 配置

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSocket 的授权支持。要使用 Java 配置来配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。例如:

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

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

这将确保:

  • (1) 任何入站 CONNECT 消息都需要有效的 CSRF 令牌才能实施同源 Policy
  • (2) 对于任何入站请求,在 simpUser Headers 属性内用该用户填充 SecurityContextHolder。
  • (3) 我们的消息需要适当的授权。具体来说,任何以“/user /”开头的入站消息都需要 ROLE_USER。有关授权的其他详细信息,请参见第 23.3 节“ WebSocket 授权”

Spring Security 还提供XML Namespace支持以保护 WebSocket。可比较的基于 XML 的配置如下所示:

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

这将确保:

  • (1) 任何入站 CONNECT 消息都需要有效的 CSRF 令牌才能实施同源 Policy
  • (2) 对于任何入站请求,在 simpUser Headers 属性内用该用户填充 SecurityContextHolder。
  • (3) 我们的消息需要适当的授权。具体来说,任何以“/user /”开头的入站消息都需要 ROLE_USER。有关授权的其他详细信息,请参见第 23.3 节“ WebSocket 授权”

23.2 WebSocket 身份验证

WebSockets 重用构建 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息。这意味着HttpServletRequest上的Principal将移交给 WebSockets。如果您使用的是 Spring Security,则HttpServletRequest上的Principal将被自动覆盖。

更具体地说,要确保用户已通过 WebSocket 应用程序的身份验证,所需要做的就是确保您将 Spring Security 设置为对基于 HTTP 的 Web 应用程序进行身份验证。

23.3 WebSocket 授权

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSocket 的授权支持。要使用 Java 配置来配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。例如:

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

    }
}

这将确保:

  • (1) 任何无目的地的消息(即消息类型为 MESSAGE 或 SUBSCRIBE 以外的任何消息)都将要求用户进行身份验证
  • (2) 任何人都可以订阅/ user/queue/errors
  • (3) 任何目标以“/app /”开头的消息都将要求用户具有 ROLE_USER 角色
  • (4) 任何以“/user /”或“/topic/friends /”开头且类型为 SUBSCRIBE 的消息都需要 ROLE_USER
  • (5) 拒绝其他任何类型为 MESSAGE 或 SUBSCRIBE 的消息。由于 6,我们不需要这一步,但是它说明了如何在特定的消息类型上进行匹配。
  • (6) 任何其他消息均被拒绝。这是确保您不会错过任何消息的好主意。

Spring Security 还提供XML Namespace支持以保护 WebSocket。可比较的基于 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) 任何以“/user /”或“/topic/friends /”开头且类型为 SUBSCRIBE 的消息都需要 ROLE_USER
  • (5) 拒绝其他任何类型为 MESSAGE 或 SUBSCRIBE 的消息。由于 6,我们不需要这一步,但是它说明了如何在特定的消息类型上进行匹配。
  • (6) 带有目的地的任何其他消息均被拒绝。这是确保您不会错过任何消息的好主意。

23.3.1 WebSocket 授权说明

为了正确保护您的应用程序,了解 Spring 的 WebSocket 支持非常重要。

WebSocket 对消息类型的授权

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

考虑聊天应用程序。

  • 系统可以通过“/topic/system/notifications”的目的地向所有用户发送“ MESSAGE”通知

  • Client 可以通过订阅接收到“/topic/system/notifications”的通知。

虽然我们希望 Client 能够订阅“/topic/system/notifications”,但我们不想让他们将 MESSAGE 发送到该目的地。如果我们允许向“/topic/system/notifications”发送消息,则 Client 端可以直接向该端点发送消息并模拟系统。

通常,应用程序通常会拒绝发送到以broker prefix开头的消息(即“/topic /”或“/queue /”)的任何 MESSAGE。

目标上的 WebSocket 授权

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

考虑聊天应用程序。

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

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

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

  • 消息变成目标“/queue/user/messages- ”

使用上面的应用程序,我们希望允许我们的 Client 端收听“/user/queue”,它被转换为“/queue/user/messages- ”。但是,我们不希望 Client 端能够收听“/queue/*”,因为那样会使 Client 端看到每个用户的消息。

通常,应用程序通常会拒绝发送到以broker prefix开头的消息(即“/topic /”或“/queue /”)的任何 SUBSCRIBE。当然,我们可能会提供 exceptions 来说明诸如

23.3.2 外发邮件

Spring 包含标题为消息流的部分,描述了消息如何在系统中流动。重要的是要注意,Spring Security 只保护clientInboundChannel。 Spring Security 不会尝试保护clientOutboundChannel

最重要的原因是性能。对于每条传入的消息,通常会有更多的出去消息。我们鼓励保护对端点的订阅,而不是保护出站消息。

23.4 强制执行相同来源策略

需要强调的是,浏览器不会对 WebSocket 连接强制执行同源 Policy。这是一个非常重要的考虑因素。

23.4.1 为什么来源相同?

请考虑以下情形。用户访问 bank.com 并验证其帐户。同一用户在其浏览器中打开另一个选项卡,并访问 evil.com。相同来源 Policy 可确保 evil.com 无法读取数据或将数据写入 bank.com。

对于 WebSocket,不适用“相同来源策略”。实际上,除非 bank.com 明确禁止这样做,否则 evil.com 可以代表用户读取和写入数据。这意味着用户可以通过 webSocket 执行任何操作(即转帐),evil.com 可以代表该用户执行操作。

由于 SockJS 尝试模拟 WebSocket,因此它也绕过了相同起源策略。这意味着开发人员在使用 SockJS 时需要明确保护其应用程序不受外部域的影响。

23.4.2 Spring WebSocket 允许的来源

幸运的是,从 Spring 4.1.5 开始,Spring 的 WebSocket 和 SockJS 支持限制了对current domain的访问。 Spring Security 增加了一层额外的保护来提供纵深防御

23.4.3 将 CSRF 添加到 StompHeaders

默认情况下,Spring Security 需要任何 CONNECT 消息类型中的CSRF token。这样可以确保只有有权访问 CSRF 令牌的站点才能连接。由于只有 Same Origin 可以访问 CSRF 令牌,因此不允许外部域进行连接。

通常,我们需要在 HTTP Headers 或 HTTP 参数中包含 CSRF 令牌。但是,SockJS 不允许使用这些选项。相反,我们必须在 Stomp Headers 中包含令牌

应用程序可以通过访问名为_csrf 的请求属性来获取 CSRF 令牌。例如,以下将允许在 JSP 中访问CsrfToken

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

如果使用的是静态 HTML,则可以在 REST 端点上公开CsrfToken。例如,以下代码将在 URL/csrf 上显示CsrfToken

@RestController
public class CsrfController {

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

JavaScript 可以对端点进行 REST 调用,并使用响应填充 headerName 和令牌。

现在,我们可以将令牌包含在 Stomp Client 端中。例如:

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

}

23.4.4 在 WebSocket 中禁用 CSRF

如果您想允许其他域访问您的站点,则可以禁用 Spring Security 的保护。例如,在 Java 配置中,您可以使用以下代码:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ...

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

23.5 使用 SockJS

SockJS提供后备传输以支持较旧的浏览器。使用后备选项时,我们需要放宽一些安全性约束,以允许 SockJS 与 Spring Security 一起使用。

23.5.1 SockJS 和框架选项

SockJS 可以使用利用 iframe 进行运输。默认情况下,Spring Security 将对网站进行denyFramework,以防止 Clickjacking 攻击。为了使基于 SockJS 框架的传输能够正常工作,我们需要配置 Spring Security 以允许相同的来源对内容进行框架化。

您可以使用frame-options元素自定义 X-Frame-Options。例如,以下内容将指示 Spring Security 使用“ X-Frame-Options:SAMEORIGIN”,它允许同一域内的 iframe:

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

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

同样,您可以使用以下方法自定义框架选项以在 Java 配置中使用相同的来源:

@EnableWebSecurity
public class WebSecurityConfig extends
   WebSecurityConfigurerAdapter {

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

23.5.2 SockJS 和令人放松的 CSRF

SockJS 在 CONNECT 消息上使用 POST 进行任何基于 HTTP 的传输。通常,我们需要在 HTTPHeaders 或 HTTP 参数中包含 CSRF 令牌。但是,SockJS 不允许使用这些选项。相反,我们必须按照第 23.4.3 节“将 SRF 添加到 StompHeaders 中”中的说明在 StompHeaders 中包含令牌。

这也意味着我们需要通过 Web 层放宽对 CSRF 的保护。具体来说,我们要为连接 URL 禁用 CSRF 保护。我们不想禁用每个 URL 的 CSRF 保护。否则,我们的站点将容易受到 CSRF 攻击。

通过提供 CSRF RequestMatcher,我们可以轻松实现这一目标。我们的 Java 配置非常简单。例如,如果我们的踩踏端点是“/chat”,则可以使用以下配置仅对以“/chat /”开头的 URL 禁用 CSRF 保护:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
    extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        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()

            ...

如果使用基于 XML 的配置,则可以使用[email protected]。例如:

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