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

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

    • 项目地址
    • 添加依赖
    • 主配置类
    • 开发中遇到的坑

项目地址

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

添加依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-websocketartifactId>
dependency>

<dependency>
    <groupId>org.springframework.securitygroupId>
    <artifactId>spring-security-messagingartifactId>
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");
    }

}

开发中遇到的坑

  1. 通过参考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授权决策检查

  1. 跨域连接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());
}
  1. 虽然支持了@MessageMapping,但是我仍然使用@RequestMapping发送消息,只在浏览器订阅消息时使用websocket,因为项目中配置了validation和全局异常拦截,通过全局异常拦截向浏览器返回提示信息,如果用MessageMapping,参数验证不通过的话会直接抛异常,而不会被全局异常拦截器拦截。

如果有误导人的地方,欢迎大神批评指正!

你可能感兴趣的:(spring)