WebSocket 安全性

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

直接 JSR-356 支持

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

WebSocket 身份验证

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

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

WebSocket 授权

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSockets 的授权支持。spring-doc.cn

在 Spring Security 5.8 中,此支持已刷新以使用 API。AuthorizationManagerspring-doc.cn

要使用 Java 配置配置授权,只需包含注释并发布一个 bean,或者在 XML 中使用该属性。 实现此目的的一种方法是使用 to specify endpoint patterns,如下所示:@EnableWebSocketSecurityAuthorizationManager<Message<?>>use-authorization-managerAuthorizationManagerMessageMatcherRegistryspring-doc.cn

@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 在任何入站请求的 header 属性中填充 user。SecurityContextHoldersimpUser
3 我们的消息需要适当的授权。具体而言,任何以 开头的入站消息都需要 .您可以在 WebSocket 授权中找到有关授权的其他详细信息/user/ROLE_USER

自定义授权

使用 时,自定义非常简单。 例如,您可以使用 发布一个要求所有消息都具有 “USER” 角色的 ,如下所示:AuthorizationManagerAuthorizationManagerAuthorityAuthorizationManagerspring-doc.cn

@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"/>

有几种方法可以进一步匹配消息,如下面的更高级示例所示:spring-doc.cn

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

这将确保:spring-doc.cn

1 任何没有目的地的消息(即 MESSAGE 或 SUBSCRIBE 的 Message 类型以外的任何消息)都需要对用户进行身份验证
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.cn

消息类型的 WebSocket 授权

您需要了解消息之间的区别和类型以及它们在 Spring 中是如何工作的。SUBSCRIBEMESSAGEspring-doc.cn

考虑一个聊天应用程序:spring-doc.cn

  • 系统可以通过目标 向所有用户发送通知。MESSAGE/topic/system/notificationsspring-doc.cn

  • 客户端可以通过以下方式接收通知。SUBSCRIBE/topic/system/notificationsspring-doc.cn

虽然我们希望客户端能够 ,但我们不希望它们能够将 发送到该目标。 如果我们允许发送 to ,则客户端可以直接向该终端节点发送消息并模拟系统。SUBSCRIBE/topic/system/notificationsMESSAGEMESSAGE/topic/system/notificationsspring-doc.cn

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

目标上的 WebSocket 授权

您还应该了解目标是如何转换的。spring-doc.cn

考虑一个聊天应用程序:spring-doc.cn

  • 用户可以通过向目标发送消息来向特定用户发送消息。/app/chatspring-doc.cn

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

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

  • 邮件将转换为 的目标。/queue/user/messages-<sessionid>spring-doc.cn

通过这个聊天应用程序,我们希望让我们的客户端 倾听 ,它被转换为 。 但是,我们不希望客户端能够监听 ,因为这会让客户端看到每个用户的消息。/user/queue/queue/user/messages-<sessionid>/queue/*spring-doc.cn

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

出站消息

Spring Framework 参考文档包含一个名为“消息流”的部分,该部分描述了消息如何流经系统。 请注意,Spring Security 仅保护 . Spring Security 不会尝试保护 .clientInboundChannelclientOutboundChannelspring-doc.cn

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

实施同源策略

请注意,浏览器不会对 WebSocket 连接强制实施同源策略。 这是一个极其重要的考虑因素。spring-doc.cn

为什么选择 Same Origin?

请考虑以下场景。 用户访问其账户并对其进行身份验证。 同一用户在其浏览器中打开另一个选项卡并访问 。 同源策略可确保 无法从 读取数据或向 .bank.comevil.comevil.combank.comspring-doc.cn

对于 WebSockets,同源策略不适用。 其实,除非明确禁止,否则可以代为读写数据。 这意味着用户可以通过 webSocket 做的任何事情(例如转账)都可以代表该用户做。bank.comevil.comevil.comspring-doc.cn

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

Spring WebSocket 允许的来源

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

将 CSRF 添加到 Stomp 标头

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

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

应用程序可以通过访问名为 的请求属性来获取 CSRF 令牌。 例如,以下允许访问 JSP 中的 :_csrfCsrfTokenspring-doc.cn

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

如果使用静态 HTML,则可以在 REST 端点上公开 。 例如,以下内容将公开 URL 上的:CsrfTokenCsrfToken/csrfspring-doc.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 调用,并使用响应填充 和 令牌。headerNamespring-doc.cn

现在,我们可以将令牌包含在 Stomp 客户端中:spring-doc.cn

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

})

在 WebSockets 中禁用 CSRF

此时,CSRF 在使用 时是不可配置的,尽管这可能会在未来的版本中添加。@EnableWebSocketSecurity

要禁用 CSRF,而不是使用 ,您可以使用 XML 支持或自己添加 Spring Security 组件,如下所示:@EnableWebSocketSecurityspring-doc.cn

@Configuration
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {

    private final ApplicationContext applicationContext;

    private final AuthorizationManager<Message<?>> authorizationManager;

    public WebSocketSecurityConfig(ApplicationContext applicationContext, AuthorizationManager<Message<?>> authorizationManager) {
        this.applicationContext = applicationContext;
        this.authorizationManager = authorizationManager;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(authorizationManager);
        AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(applicationContext);
        authz.setAuthorizationEventPublisher(publisher);
        registration.interceptors(new SecurityContextChannelInterceptor(), authz);
    }
}
@Configuration
open class WebSocketSecurityConfig(val applicationContext: ApplicationContext, val authorizationManager: AuthorizationManager<Message<*>>) : WebSocketMessageBrokerConfigurer {
    @Override
    override fun addArgumentResolvers(argumentResolvers: List<HandlerMethodArgumentResolver>) {
        argumentResolvers.add(AuthenticationPrincipalArgumentResolver())
    }

    @Override
    override fun configureClientInboundChannel(registration: ChannelRegistration) {
        var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(authorizationManager)
        var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(applicationContext)
        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 配置中,您可以使用以下内容:spring-doc.cn

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ...

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {

    // ...

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

自定义表达式处理程序

有时,自定义 XML 元素中定义的表达式的处理方式可能很有价值。 为此,您可以创建一个类型的类,并在 XML 定义中引用它,如下所示:accessintercept-messageSecurityExpressionHandler<MessageAuthorizationContext<?>>spring-doc.cn

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

如果要从实现 的旧式用法迁移,则可以: 1. 另外实现该方法,然后 2. 将该值包装成这样:websocket-message-brokerSecurityExpressionHandler<Message<?>>createEvaluationContext(Supplier, Message)MessageAuthorizationContextSecurityExpressionHandlerspring-doc.cn

<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 一起工作。spring-doc.cn

SockJS & 框架选项

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

您可以使用 frame-options 元素进行自定义。 例如,以下内容指示 Spring Security 使用 ,它允许同一域内的 iframe:X-Frame-OptionsX-Frame-Options: SAMEORIGINspring-doc.cn

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

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

同样,您可以使用以下内容自定义框架选项以在 Java 配置中使用相同的来源:spring-doc.cn

@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 对 CONNECT 消息使用 POST 进行任何基于 HTTP 的传输。 通常,我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF 令牌。 但是,Sockjs 不允许这些选项。 相反,我们必须在 Stomp 标头中包含令牌,如 将 CSRF 添加到 Stomp 标头中所述。spring-doc.cn

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

我们可以通过提供 CSRF 轻松实现这一点。 我们的 Java 配置使这变得简单。 例如,如果我们的 stomp 端点是 ,我们只能使用以下配置对以 开头的 URL 禁用 CSRF 保护:RequestMatcher/chat/chat/spring-doc.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
                .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-refspring-doc.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>

旧版 WebSocket 配置

在 Spring Security 5.8 之前,使用 Java 配置配置消息传递授权的方法是扩展和配置 . 例如:AbstractSecurityWebSocketMessageBrokerConfigurerMessageSecurityMetadataSourceRegistryspring-doc.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.cn

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

如果您有一个扩展和覆盖或 的自定义,则使用旧版配置会很有帮助。 为了延迟查找,新 API 在计算表达式时不会调用这些 API。SecurityExpressionHandlerAbstractSecurityExpressionHandlercreateEvaluationContextInternalcreateSecurityExpressionRootAuthorizationAuthorizationManagerspring-doc.cn

如果您使用的是 XML,则只需不使用该元素或将其设置为 .use-authorization-managerfalsespring-doc.cn