springboot2.1.3整合websocket和websocket-security支持跨域连接

springboot整合websocket和websocket-security支持跨域连接

项目地址: https://gitee.com/xuelingkang/spring-boot-demo
完整配置参考com.example.websocket包

<!-- websocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- websocket security -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-messaging</artifactId>
</dependency>
package com.example.config;
import com.example.websocket.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebMvcStompEndpointRegistry;
import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler;
import org.springframework.web.socket.messaging.StompSubProtocolHandler;
 * 默认通过注解{@link EnableWebSocketMessageBroker}
 * 开启使用STOMP协议来传输基于代理(message broker)的消息,支持使用{@link MessageMapping}就像支持{@link RequestMapping}一样。
 * 但是注解{@link EnableWebSocketMessageBroker}会引入{@link DelegatingWebSocketMessageBrokerConfiguration}配置类,
 * 该配置类默认使用{@link WebMvcStompEndpointRegistry},{@link WebMvcStompEndpointRegistry}的stomp协议处理器为
 * {@link StompSubProtocolHandler},处理消息的方法:
 * @see StompSubProtocolHandler#handleMessageFromClient(WebSocketSession, WebSocketMessage, MessageChannel)
 * @see StompSubProtocolHandler#handleMessageToClient(WebSocketSession, Message)
 * 未对自定义拦截做支持,
 * 所以取消{@link EnableWebSocketMessageBroker},使用自定义配置{@link CustomizeWebSocketMessageBrokerConfiguration}
@Configuration
//@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    // 异常处理器
    @Bean
    public StompSubProtocolErrorHandler stompSubProtocolErrorHandler() {
        return new StompSubProtocolErrorHandlerImpl();
    // 匹配消息资源
    @Bean
    public MessageResourceMatcher messageResourceMatcher() {
        return new MessageResourceMatcher();
    // 授权决策拦截器
    @Bean
    public AccessDecisionFromClientInterceptor accessDecisionFromClientInterceptor() {
        return new AccessDecisionFromClientInterceptor();
    // sessionId记录
    @Bean
    public SessionIdRegistry sessionIdRegistry() {
        return new SessionIdRegistry();
    // sessionId登记拦截器
    @Bean
    public SessionIdRegistryInterceptor sessionIdRegistryInterceptor() {
        return new SessionIdRegistryInterceptor();
    // sessionId移除拦截器
    @Bean
    public SessionIdUnRegistryInterceptor sessionIdUnRegistryInterceptor() {
        return new SessionIdUnRegistryInterceptor();
     * 只设置{@link #sameOriginDisabled}不能解决同源策略,
     * 必须在注册endpoint时设置setAllowedOrigins
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").setAllowedOrigins("*").withSockJS();
        registry.setErrorHandler(stompSubProtocolErrorHandler());
        // 配置拦截器
        ((CustomizeWebMvcStompEndpointRegistry) registry)
                .addFromClientInterceptor(accessDecisionFromClientInterceptor())
                .addFromClientInterceptor(sessionIdUnRegistryInterceptor())
                .addToClientInterceptor(sessionIdRegistryInterceptor());
    // 这里取消所有检查,统一在授权决策拦截器中处理
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages.nullDestMatcher().authenticated()
                .anyMessage().permitAll();
    // 关闭同源策略
    @Override
    protected boolean sameOriginDisabled() {
        return true;
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");

开发中遇到的坑

通过参考spring官方文档和阅读源码发现:websocket授权决策只支持硬编码,且没有对自定义拦截做支持

官方文档片段:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .nullDestMatcher().authenticated()
                .simpSubscribeDestMatchers("/user/queue/errors").permitAll()
                .simpDestMatchers("/app/**").hasRole("USER")
                .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER")
                .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll()
                .anyMessage().denyAll();

所以继承了StompSubProtocolHandler,WebMvcStompEndpointRegistry,DelegatingWebSocketMessageBrokerConfiguration这三个类,添加websocket自定义拦截器接口,可以在拦截器中自定义websocket授权决策检查

  • 跨域连接websocket,由于前后端分离开发,开发环境下前后端不同源,所以需要跨域连接websocet
  • @Override
    protected boolean sameOriginDisabled() {
        return true;
    

    配置类可以重写这个方法,默认该方法返回false,看方法的名字是关闭同源策略,但是只重写这个方法不能解决跨域的问题,还需要在registerStompEndpoints方法中设置允许的域名,"*"代表所有

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").setAllowedOrigins("*").withSockJS();
        registry.setErrorHandler(stompSubProtocolErrorHandler());
        // 配置拦截器
        ((CustomizeWebMvcStompEndpointRegistry) registry)
                .addFromClientInterceptor(accessDecisionFromClientInterceptor())
                .addFromClientInterceptor(sessionIdUnRegistryInterceptor())
                .addToClientInterceptor(sessionIdRegistryInterceptor());