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.SUBSCRIBE
的 SimpDestinationMessageMatcher
列表:
.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 支持和框架选项
通常结合使用 STOMP 和 SockJS 实现 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 的 WebSocket 和 WebSocket 安全 官方文档。
示例代码可在 我们的 GitHub 项目 中找到。