项目地址
项目地址: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());
}
- 虽然支持了@MessageMapping,但是我仍然使用@RequestMapping发送消息,只在浏览器订阅消息时使用websocket,因为项目中配置了validation和全局异常拦截,通过全局异常拦截向浏览器返回提示信息,如果用MessageMapping,参数验证不通过的话会直接抛异常,而不会被全局异常拦截器拦截。
如果有误导人的地方,欢迎大神批评指正!