对于最新的稳定版本,请使用 Spring Security 6.5.3spring-doc.cadn.net.cn

WebSocket 安全性

Spring Security 4 添加了对保护 Spring 的 WebSocket 支持的支持。 本节介绍如何使用 Spring Security 的 WebSocket 支持。spring-doc.cadn.net.cn

直接 JSR-356 支持

Spring Security 不提供直接的 JSR-356 支持,因为这样做几乎没有价值。 这是因为格式未知,因此 Spring 几乎无法保护未知格式。 此外,JSR-356 没有提供拦截消息的方法,因此安全性将相当具有侵入性。spring-doc.cadn.net.cn

WebSocket 配置

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSockets 的授权支持。 要使用 Java 配置配置授权,只需将AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry. 例如:spring-doc.cadn.net.cn

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

这将确保:spring-doc.cadn.net.cn

1 任何入站 CONNECT 消息都需要有效的 CSRF Tokens才能强制执行同源策略
2 SecurityContextHolder 在 simpUser 标头属性中填充了任何入站请求的用户。
3 我们的消息需要适当的授权。具体来说,任何以“/user/”开头的入站消息都需要ROLE_USER。有关授权的其他详细信息,请参阅 WebSocket 授权

Spring Security还提供了XML命名空间支持来保护WebSocket。 基于 XML 的类似配置如下所示:spring-doc.cadn.net.cn

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

这将确保:spring-doc.cadn.net.cn

1 任何入站 CONNECT 消息都需要有效的 CSRF Tokens才能强制执行同源策略
2 SecurityContextHolder 在 simpUser 标头属性中填充了任何入站请求的用户。
3 我们的消息需要适当的授权。具体来说,任何以“/user/”开头的入站消息都需要ROLE_USER。有关授权的其他详细信息,请参阅 WebSocket 授权

WebSocket 身份验证

WebSocket 重用建立 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息。 这意味着PrincipalHttpServletRequest将移交给 WebSockets。 如果您使用的是 Spring Security,则PrincipalHttpServletRequest会自动覆盖。spring-doc.cadn.net.cn

更具体地说,要确保用户已对您的 WebSocket 应用程序进行身份验证,所需要做的就是确保您设置 Spring Security 来验证基于 HTTP 的 Web 应用程序。spring-doc.cadn.net.cn

WebSocket 授权

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSockets 的授权支持。 要使用 Java 配置配置授权,只需将AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry. 例如:spring-doc.cadn.net.cn

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

这将确保:spring-doc.cadn.net.cn

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命名空间支持来保护WebSocket。 基于 XML 的类似配置如下所示:spring-doc.cadn.net.cn

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

这将确保:spring-doc.cadn.net.cn

1 任何类型的 CONNECT、UNSUBSCRIBE 或 DISCONNECT 消息都需要对用户进行身份验证
2 任何人都可以订阅 /user/queue/errors
3 任何目标以“/app/”开头的消息都要求用户具有角色ROLE_USER
4 任何以“/user/”或“/topic/friends/”开头且类型为 SUBSCRIBE 的消息都需要ROLE_USER
5 拒绝 MESSAGE 或 SUBSCRIBE 类型的任何其他消息。由于 6,我们不需要此步骤,但它说明了如何匹配特定的消息类型。
6 拒绝任何其他具有目标的邮件。这是确保您不会错过任何消息的好主意。

WebSocket 授权说明

为了正确保护您的应用程序,了解 Spring 的 WebSocket 支持非常重要。spring-doc.cadn.net.cn

消息类型的 WebSocket 授权

了解 SUBSCRIBE 和 MESSAGE 类型的消息之间的区别及其在 Spring 中的工作原理非常重要。spring-doc.cadn.net.cn

考虑一个聊天应用程序。spring-doc.cadn.net.cn

  • 系统可以通过“/topic/system/notifications”的目的地向所有用户发送通知 MESSAGEspring-doc.cadn.net.cn

  • 客户端可以通过订阅“/topic/system/notifications”来接收通知。spring-doc.cadn.net.cn

虽然我们希望客户端能够订阅“/topic/system/notifications”,但我们不希望它们能够向该目的地发送消息。 如果我们允许向“/topic/system/notifications”发送消息,那么客户端可以直接向该端点发送消息并模拟系统。spring-doc.cadn.net.cn

通常,应用程序通常会拒绝发送到以代理前缀开头的目的地(即“/topic/”或“/queue/”)的任何MESSAGE。spring-doc.cadn.net.cn

目标上的WebSocket授权

了解目的地是如何转变的也很重要。spring-doc.cadn.net.cn

考虑一个聊天应用程序。spring-doc.cadn.net.cn

  • 用户可以通过向“/app/chat”的目的地发送消息来向特定用户发送消息。spring-doc.cadn.net.cn

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

  • 然后,应用程序使用SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message).spring-doc.cadn.net.cn

  • 消息将转换为“/queue/user/messages-<sessionid>”的目标spring-doc.cadn.net.cn

通过上面的应用程序,我们希望允许我们的客户端监听“/user/queue”,它被转换为“/queue/user/messages-<sessionid>”。 但是,我们不希望客户端能够监听“/queue/*”,因为这将允许客户端查看每个用户的消息。spring-doc.cadn.net.cn

通常,应用程序通常会拒绝发送到以代理前缀(即“/topic/”或“/queue/”)开头的消息的任何SUBSCRIBE。 当然,我们可能会提供例外情况来考虑以下情况spring-doc.cadn.net.cn

出站消息

Spring包含一个标题为“消息流”的部分,该部分描述了消息如何在系统中流动。 请务必注意,Spring Security 仅保护clientInboundChannel. Spring Security 不会尝试保护clientOutboundChannel.spring-doc.cadn.net.cn

造成这种情况的最重要原因是性能。 对于每条传入的消息,通常都会有更多的消息传出。 我们鼓励保护终结点的订阅,而不是保护出站消息。spring-doc.cadn.net.cn

执行同源策略

需要强调的是,浏览器不会对 WebSocket 连接强制执行同源策略。 这是一个极其重要的考虑因素。spring-doc.cadn.net.cn

为什么是同源?

请考虑以下场景。 用户访问 bank.com 并对其帐户进行身份验证。 同一用户在浏览器中打开另一个选项卡并访问 evil.com。 同源策略可确保 evil.com 无法读取或写入数据 bank.com。spring-doc.cadn.net.cn

对于 WebSocket,同源策略不适用。 事实上,除非 bank.com 明确禁止,否则 evil.com 可以代表用户读取和写入数据。 这意味着用户可以通过 webSocket 执行的任何作(即转账),evil.com 都可以代表该用户执行。spring-doc.cadn.net.cn

由于 SockJS 尝试模拟 WebSocket,因此它也绕过了同源策略。 这意味着开发人员在使用 SockJS 时需要明确保护他们的应用程序免受外部域的影响。spring-doc.cadn.net.cn

Spring WebSocket 允许的来源

幸运的是,从 Spring 4.1.5 开始,Spring 的 WebSocket 和 SockJS 支持限制了对当前域的访问。 Spring Security 增加了额外的保护层,以提供深度防御spring-doc.cadn.net.cn

将 CSRF 添加到 Stomp 标头

默认情况下,Spring Security 需要任何 CONNECT 消息类型中的 CSRF Tokens。 这确保只有有权访问 CSRF Tokens的站点才能连接。 由于只有同源可以访问 CSRF Tokens,因此不允许外部域建立连接。spring-doc.cadn.net.cn

通常,我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF Tokens。 但是,SockJS 不允许这些选项。 相反,我们必须在 Stomp 标头中包含Tokensspring-doc.cadn.net.cn

应用程序可以通过访问名为 _csrf 的请求属性来获取 CSRF Tokens。 例如,以下内容将允许访问CsrfToken在 JSP 中:spring-doc.cadn.net.cn

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

如果您使用的是静态 HTML,则可以公开CsrfToken在 REST 端点上。 例如,以下内容将公开CsrfToken在 URL /csrf 上spring-doc.cadn.net.cn

@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 和Tokens。spring-doc.cadn.net.cn

我们现在可以将Tokens包含在我们的 Stomp 客户端中。 例如:spring-doc.cadn.net.cn

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

}

在 WebSockets 中禁用 CSRF

如果要允许其他域访问您的站点,可以禁用 Spring Security 的保护。 例如,在 Java 配置中,您可以使用以下内容:spring-doc.cadn.net.cn

@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 一起使用。spring-doc.cadn.net.cn

SockJS 和框架选项

SockJS 可以使用利用 iframe 的传输。 默认情况下,Spring Security 将拒绝对站点进行框架以防止点击劫持攻击。 为了允许 SockJS 基于帧的传输工作,我们需要配置 Spring Security 以允许同一源来构建内容。spring-doc.cadn.net.cn

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

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

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

同样,您可以使用以下命令自定义帧选项以在 Java 配置中使用相同的原点:spring-doc.cadn.net.cn

@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。 通常,我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF Tokens。 但是,SockJS 不允许这些选项。 相反,我们必须将Tokens包含在 Stomp 标头中,如 将 CSRF 添加到 Stomp 标头中所述。spring-doc.cadn.net.cn

这也意味着我们需要放松对 Web 层的 CSRF 保护。 具体来说,我们希望禁用连接 URL 的 CSRF 保护。 我们不想为每个 URL 禁用 CSRF 保护。 否则,我们的网站将容易受到 CSRF 攻击。spring-doc.cadn.net.cn

我们可以通过提供 CSRF RequestMatcher 轻松实现这一点。 我们的 Java 配置使这变得非常简单。 例如,如果我们的 stomp 端点是“/chat”,我们可以使用以下配置仅对以“/chat/”开头的 URL 禁用 CSRF 保护:spring-doc.cadn.net.cn

@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。 例如:spring-doc.cadn.net.cn

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