1. 介绍

上一篇文章中,我们展示了如何为 Spring MVC 项目添加 WebSocket 支持。本文将重点介绍如何为 Spring MVC 中的 WebSocket 添加安全机制。在继续之前,请确保你已经配置了基础的 Spring MVC 安全机制——如果还没有,可以参考这篇文章

2. Maven 依赖

实现 WebSocket 安全需要两组核心 Maven 依赖

首先,指定我们将使用的 Spring Framework 和 Spring Security 版本:

<properties>
    <spring.version>6.0.12</spring.version>
    <spring-security.version>6.1.5</spring-security.version>
    <spring-security-messaging.version>6.0.2</spring-security-messaging.version>
</properties>

其次,添加实现基础认证和授权所需的核心 Spring MVC 和 Spring Security 库:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring-security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring-security.version}</version>
</dependency>

最后,添加 WebSocket 相关的必需依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-messaging</artifactId>
    <version>${spring-security-messaging.version}</version>
</dependency>

3. 基础 WebSocket 安全

使用 spring-security-messaging 库实现 WebSocket 安全的核心是 AbstractSecurityWebSocketMessageBrokerConfigurer 类及其在项目中的实现:

@Configuration
public class SocketSecurityConfig 
  extends AbstractSecurityWebSocketMessageBrokerConfigurer {
      //...
}

AbstractSecurityWebSocketMessageBrokerConfigurer提供了 WebSecurityConfigurerAdapter 之外的额外安全覆盖

虽然 spring-security-messaging 不是实现 WebSocket 安全的唯一方式(也可以使用 spring-websocket 库实现 WebSocketConfigurer 接口),但本文采用 AbstractSecurityWebSocketMessageBrokerConfigurer 方案。

3.1. 实现 configureInbound()

configureInbound() 的实现是配置 AbstractSecurityWebSocketMessageBrokerConfigurer 子类的最关键步骤

@Override 
protected void configureInbound(
  MessageSecurityMetadataSourceRegistry messages) { 
    messages
      .simpDestMatchers("/secured/**").authenticated()
      .anyMessage().authenticated(); 
}

WebSecurityConfigurerAdapter 指定不同路由的应用级授权要求不同,AbstractSecurityWebSocketMessageBrokerConfigurer 允许你为 Socket 目标指定特定的授权要求。

3.2. 类型和目标匹配

MessageSecurityMetadataSourceRegistry 允许指定安全约束,如路径、用户角色和允许的消息类型。

类型匹配器约束允许的 SimpMessageType 及其方式

.simpTypeMatchers(CONNECT, UNSUBSCRIBE, DISCONNECT).permitAll()

目标匹配器约束可访问的接口模式及其方式

.simpDestMatchers("/app/**").hasRole("ADMIN")

订阅目标匹配器映射匹配 SimpMessageType.SUBSCRIBESimpDestinationMessageMatcher 列表

.simpSubscribeDestMatchers("/topic/**").authenticated()

完整方法列表可参考官方文档

4. 保护 Socket 路由

现在我们了解了基础 Socket 安全和类型匹配配置,接下来将结合 Socket 安全、视图、STOMP(文本消息协议)、消息代理和 Socket 控制器,在 Spring MVC 应用中启用安全的 WebSocket。

首先,为 Socket 视图和控制器设置基础 Spring 安全覆盖:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
@ComponentScan("com.baeldung.springsecuredsockets")
public class SecurityConfig {

    /**
     * 优先级顺序至关重要
     * <p>
     * 匹配从上到下进行——最顶层的匹配优先成功
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
                        authorizationManagerRequestMatcherRegistry
                                .requestMatchers("/", "/index", "/authenticate").permitAll()
                                .requestMatchers("/secured/**/**", "/secured/**/**/**", "/secured/socket", "/secured/success").authenticated()
                                .anyRequest().authenticated())
            .formLogin(httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer.loginPage("/login").permitAll()
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .loginProcessingUrl("/authenticate")
                    .successHandler(loginSuccessHandler())
                    .failureUrl("/denied").permitAll())
            //...
    }
}

其次,为消息目标设置认证要求:

@Configuration
public class SocketSecurityConfig 
  extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
          .simpDestMatchers("/secured/**").authenticated()
          .anyMessage().authenticated();
    }   
}

WebSocketMessageBrokerConfigurer 中注册实际消息和 STOMP 接口:

@Configuration
@EnableWebSocketMessageBroker
public class SocketBrokerConfig 
  implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/secured/history");
        config.setApplicationDestinationPrefixes("/spring-security-mvc-socket");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/secured/chat")
          .withSockJS();
    }
}

定义示例 Socket 控制器和接口:

@Controller
public class SocketController {
 
    @MessageMapping("/secured/chat")
    @SendTo("/secured/history")
    public OutputMessage send(Message msg) throws Exception {
        return new OutputMessage(
           msg.getFrom(),
           msg.getText(), 
           new SimpleDateFormat("HH:mm").format(new Date())); 
    }
}

5. 同源策略

同源策略要求与接口的所有交互必须来自发起交互的同一域名。

例如,假设你的 WebSocket 实现托管在 foo.com,且强制执行同源策略。如果用户通过 foo.com 的客户端连接,然后在另一个浏览器打开 bar.com,则 bar.com 将无法访问你的 WebSocket 实现。

5.1. 覆盖同源策略

Spring WebSocket 默认强制执行同源策略,而普通 WebSocket 则不会

实际上,Spring Security 要求任何有效的 CONNECT 消息类型都必须包含 CSRF(跨站请求伪造)令牌

@Controller
public class CsrfTokenController {
    @GetMapping("/csrf")
    public @ResponseBody String getCsrfToken(HttpServletRequest request) {
        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        return csrf.getToken();
    }
}

通过调用 /csrf 接口,客户端可获取令牌并通过 CSRF 安全层进行认证。

但可以通过在 AbstractSecurityWebSocketMessageBrokerConfigurer 中添加以下配置覆盖 Spring 的同源策略

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

5.2. STOMP、SockJS 支持和框架选项

通常结合使用 STOMPSockJS 实现 Spring WebSocket 的客户端支持。

SockJS 默认禁止通过 HTML iframe 元素传输,这是为了防止点击劫持威胁

但在某些用例中,允许 iframe 利用 SockJS 传输可能更有利。可通过创建 SecurityFilterChain bean 实现:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) 
  throws Exception {
    http
      .csrf(AbstractHttpConfigurer::disable)
        //...
      .headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
      .authorizeHttpRequests(Customizer.withDefaults());
    return http.build();
}

⚠️ 注意:此示例中尽管允许通过 iframe 传输,但仍遵循同源策略

6. OAuth2 支持

通过在标准 WebSecurityConfigurerAdapter 基础上扩展 OAuth2 安全覆盖,可为 Spring WebSocket 提供特定的 OAuth2 支持。这里 有 OAuth2 实现示例。

要从客户端认证并访问 WebSocket 接口,可在连接时将 OAuth2 access_token 作为查询参数传递:

var endpoint = '/ws/?access_token=' + auth.access_token;
var socket = new SockJS(endpoint);
var stompClient = Stomp.over(socket);

7. 总结

本教程简要介绍了如何为 Spring WebSocket 添加安全机制。如需深入了解,可参考 Spring 的 WebSocketWebSocket 安全 官方文档。

示例代码可在 我们的 GitHub 项目 中找到。


原始标题:Intro to Security and WebSockets