此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

Spring Security 4 增加了对保护 Spring 的 WebSocket 支持的支持。 本节介绍如何使用 Spring Security 的 WebSocket 支持。Spring中文文档

直接支持 JSR-356

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

WebSocket 身份验证

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

更具体地说,为了确保用户已经对你的 WebSocket 应用程序进行了身份验证,只需要确保你设置 Spring Security 来验证你基于 HTTP 的 Web 应用程序。Spring中文文档

WebSocket 授权

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSocket 的授权支持。Spring中文文档

在 Spring Security 5.8 中,此支持已更新为使用 API。AuthorizationManagerSpring中文文档

要使用 Java 配置配置授权,只需包含注释并发布 Bean,或者在 XML 中使用该属性。 执行此操作的一种方法是使用 指定端点模式,如下所示:@EnableWebSocketSecurityAuthorizationManager<Message<?>>use-authorization-managerAuthorizationManagerMessageMatcherRegistrySpring中文文档

@Configuration
@EnableWebSocketSecurity (1) (2)
public class WebSocketSecurityConfig {

    @Bean
    AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
        messages
                .simpDestMatchers("/user/**").authenticated() (3)

        return messages.build();
    }
}
@Configuration
@EnableWebSocketSecurity (1) (2)
open class WebSocketSecurityConfig { (1) (2)
    @Bean
    fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
        messages.simpDestMatchers("/user/**").authenticated() (3)
        return messages.build()
    }
}
1 任何入站 CONNECT 消息都需要有效的 CSRF 令牌来强制执行同源策略
2 在任何入站请求的 header 属性中填充用户。SecurityContextHoldersimpUser
3 我们的消息需要适当的授权。具体而言,任何以 will 开头的入站消息都需要 .您可以在 WebSocket 授权中找到有关授权的其他详细信息/user/ROLE_USER

Spring Security 还提供 XML 命名空间支持,用于保护 WebSocket。 基于 XML 的类似配置如下所示:Spring中文文档

<websocket-message-broker use-authorization-manager="true"> (1) (2)
    <intercept-message pattern="/user/**" access="authenticated"/> (3)
</websocket-message-broker>

这将确保:Spring中文文档

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

自定义授权

使用时,自定义非常简单。 例如,您可以使用 发布一个要求所有消息都具有“USER”角色的邮件,如下所示:AuthorizationManagerAuthorizationManagerAuthorityAuthorizationManagerSpring中文文档

@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中文文档

@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中文文档

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

WebSocket 授权说明

为了正确保护你的应用程序,你需要了解Spring的WebSocket支持。Spring中文文档

消息类型的 WebSocket 授权

您需要了解消息和消息类型之间的区别以及它们在 Spring 中的工作方式。SUBSCRIBEMESSAGESpring中文文档

考虑一个聊天应用程序:Spring中文文档

  • 系统可以通过目标 向所有用户发送通知。MESSAGE/topic/system/notificationsSpring中文文档

  • 客户端可以通过 .SUBSCRIBE/topic/system/notificationsSpring中文文档

虽然我们希望客户端能够 ,但我们不希望他们能够将 a 发送到该目标。 如果我们允许将 发送到 ,客户端可以直接向该端点发送消息并模拟系统。SUBSCRIBE/topic/system/notificationsMESSAGEMESSAGE/topic/system/notificationsSpring中文文档

通常,应用程序通常会拒绝发送到以代理前缀 ( 或 ) 开头的目标的任何内容。MESSAGE/topic//queue/Spring中文文档

目标上的 WebSocket 授权

您还应该了解目的地是如何转换的。Spring中文文档

考虑一个聊天应用程序:Spring中文文档

  • 用户可以通过向目标发送消息来向特定用户发送消息。/app/chatSpring中文文档

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

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

  • 消息将转换为 的目标。/queue/user/messages-<sessionid>Spring中文文档

有了这个聊天应用程序,我们想让我们的客户听,它被转换为. 但是,我们不希望客户端能够侦听 ,因为这会让客户端看到每个用户的消息。/user/queue/queue/user/messages-<sessionid>/queue/*Spring中文文档

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

出站消息

Spring Framework 参考文档包含标题为“消息流”的部分,该部分描述了消息如何在系统中流动。 请注意,Spring Security 仅保护 . Spring Security 不会尝试保护 .clientInboundChannelclientOutboundChannelSpring中文文档

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

1 任何入站 CONNECT 消息都需要有效的 CSRF 令牌来强制执行同源策略
2 在任何入站请求的 header 属性中填充用户。SecurityContextHoldersimpUser
3 我们的消息需要适当的授权。具体而言,任何以 will 开头的入站消息都需要 .您可以在 WebSocket 授权中找到有关授权的其他详细信息/user/ROLE_USER
1 任何入站 CONNECT 消息都需要有效的 CSRF 令牌来强制执行同源策略
2 SecurityContextHolder 由任何入站请求的 simpUser 标头属性中的用户填充。
3 我们的消息需要适当的授权。具体而言,任何以“/user/”开头的入站消息都需要ROLE_USER。有关授权的其他详细信息,请参阅 WebSocket 授权
1 任何没有目标的消息(即 MESSAGE 或 SUBSCRIBE 的消息类型以外的任何消息)都需要对用户进行身份验证
2 任何人都可以订阅 /user/queue/errors
3 任何目标以“/app/”开头的消息都将要求用户具有ROLE_USER的角色
4 任何以 “/user/” 或 “/topic/friends/” 开头且类型为 SUBSCRIBE 的消息都需要ROLE_USER
5 MESSAGE 或 SUBSCRIBE 类型的任何其他消息都将被拒绝。由于 6,我们不需要此步骤,但它说明了如何在特定消息类型上进行匹配。
6 任何其他消息都将被拒绝。这是确保您不会错过任何消息的好主意。

实施同源策略

请注意,浏览器不会对 WebSocket 连接强制实施同源策略。 这是一个非常重要的考虑因素。Spring中文文档

为什么是同源?

请考虑以下方案。 用户访问并验证其帐户。 同一用户在其浏览器中打开另一个选项卡并访问 。 同源策略可确保无法从 中读取数据或将数据写入 。bank.comevil.comevil.combank.comSpring中文文档

对于 WebSocket,同源策略不适用。 事实上,除非明确禁止,否则可以代表用户读取和写入数据。 这意味着用户可以通过 webSocket 执行的任何操作(例如转账)都可以代表该用户执行。bank.comevil.comevil.comSpring中文文档

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

Spring WebSocket 允许的来源

幸运的是,从 Spring 4.1.5 开始,Spring 的 WebSocket 和 SockJS 支持限制了对当前域的访问。 Spring Security 增加了一层额外的保护,以提供深度防御Spring中文文档

将 CSRF 添加到 Stomp 标头

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

通常,我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF 令牌。 但是,SockJS 不允许这些选项。 相反,我们必须将令牌包含在 Stomp 标头中。Spring中文文档

应用程序可以通过访问名为 的请求属性来获取 CSRF 令牌。 例如,以下内容允许在 JSP 中访问:_csrfCsrfTokenSpring中文文档

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

如果使用静态 HTML,则可以在 REST 端点上公开 。 例如,以下内容将在 URL 上公开:CsrfTokenCsrfToken/csrfSpring中文文档

@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中文文档

我们现在可以在 Stomp 客户端中包含令牌:Spring中文文档

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

})

在 WebSockets 中禁用 CSRF

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

要禁用 CSRF,而不是使用 ,您可以使用 XML 支持或自己添加 Spring Security 组件,如下所示:@EnableWebSocketSecuritySpring中文文档

@Configuration
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        AuthorizationManager<Message<?>> myAuthorizationRules = AuthenticatedAuthorizationManager.authenticated();
        AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(myAuthorizationRules);
        AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(this.context);
        authz.setAuthorizationEventPublisher(publisher);
        registration.interceptors(new SecurityContextChannelInterceptor(), authz);
    }
}
@Configuration
open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer {
    @Override
    override fun addArgumentResolvers(argumentResolvers: List<HandlerMethodArgumentResolver>) {
        argumentResolvers.add(AuthenticationPrincipalArgumentResolver())
    }

    @Override
    override fun configureClientInboundChannel(registration: ChannelRegistration) {
        var myAuthorizationRules: AuthorizationManager<Message<?>> = AuthenticatedAuthorizationManager.authenticated()
        var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(myAuthorizationRules)
        var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(this.context)
        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中文文档

@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中文文档

<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中文文档

<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>
在这一点上,CSRF 在使用 时是不可配置的,尽管这可能会在将来的版本中添加。@EnableWebSocketSecurity

使用 SockJS

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

SockJS & frame-options

SockJS 可以使用利用 iframe 的传输。 默认情况下,Spring Security 拒绝该站点被框住以防止点击劫持攻击。 为了让 SockJS 基于帧的传输工作,我们需要配置 Spring Security 让相同的源来帧内容。Spring中文文档

您可以使用 frame-options 元素进行自定义。 例如,以下指示 Spring Security 使用 ,它允许同一域中的 iframe:X-Frame-OptionsX-Frame-Options: SAMEORIGINSpring中文文档

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

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

同样,您可以使用以下方法自定义帧选项以在 Java 配置中使用相同的源:Spring中文文档

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

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

我们可以通过提供 CSRF 轻松实现这一点。 我们的 Java 配置使这变得简单。 例如,如果我们的 stomp 端点是 ,我们只能使用以下配置对开头的 URL 禁用 CSRF 保护:RequestMatcher/chat/chat/Spring中文文档

@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中文文档

<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中文文档

@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中文文档

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

如果您有一个扩展和覆盖 或 的自定义,则使用旧配置会很有帮助。 为了延迟查找,新 API 在计算表达式时不会调用这些内容。SecurityExpressionHandlerAbstractSecurityExpressionHandlercreateEvaluationContextInternalcreateSecurityExpressionRootAuthorizationAuthorizationManagerSpring中文文档

如果您使用的是 XML,则只需不使用该元素或将其设置为 即可使用旧版 API。use-authorization-managerfalseSpring中文文档

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