SpringWebsocket 提供了支持 Websocket 协议的模块,允许客户端和服务端进行实时操作。
它支持 Stomp 支持和 SockJS。
SpringWebSocket 配置
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--与 Security 结合授权使用-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
首先我们要取消掉SpringSecurity 拦截websocket
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// 禁用CSRF和Session管理,可以根据项目需要启用或禁用
httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(AbstractHttpConfigurer::disable)
.headers().frameOptions().disable()
.and()
// 配置请求授权规则--- /websocket/** 允许所有用户访问
.authorizeHttpRequests(author ->
author.antMatchers("/admin/code", "/admin/login", "/websocket/**").permitAll()
配置 WebSocket
@EnableWebSocketMessageBroker 首先启用 Websocket 消息代理,允许处理Websocket 消息。
我们再实现 WebSocketMessageBrokerConfigurer,用于配置消息代理的各个方面。
消息代理的目标、应用程序的前缀、跨域、认证等。
@Configuration
@EnableWebSocketMessageBroker
// @Order(Ordered.HIGHEST_PRECEDENCE + 999)
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
}
配置消息代理
通过重写 configureMessageBroker 进行配置消息代理。
通过 MessageBrokerRegistry 的 enableSimpleBroker 方法配置 /topic 的代理,表示客户端可以订阅 以/topic为前缀的目标,用于接收信息。
通过 MessageBrokerRegistry的setApplicationDestinationPrefixes方法设置应用程序的前缀,客户端发送信息将以/app 为前缀,才能被服务端进行处理。
通过 setUserDestinationPrefix 方法设置点对点消息地址
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//设置客户端接收点对点消息地址的前缀,默认为 /user
config.setUserDestinationPrefix("/user");
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
配置心跳
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 创建线程池任务调度器
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 设置线程池大小
taskScheduler.setPoolSize(3);
// 设置线程名称前缀
taskScheduler.setThreadNamePrefix("wss-heartbeat-thread-");
// 初始化任务调度器
taskScheduler.initialize();
// 配置心跳
registry.enableSimpleBroker("/topic")
// 设置心跳间隔(心跳发送和接收的间隔时间)
.setHeartbeatValue(new long[]{10000, 10000})
// 使用上面创建的任务调度器来处理心跳任务
.setTaskScheduler(taskScheduler);
}
前端心跳需要和后端一致
// 创建 Stomp 客户端对象,连接到指定的 WebSocket 服务器
const client = Stomp.client('ws://localhost:4646/websocket');
// 配置心跳参数
client.heartbeat.outgoing = 10000; // 发送心跳间隔(毫秒)
client.heartbeat.incoming = 10000; // 接收心跳间隔(毫秒)
// 配置重新连接延迟(毫秒)
client.reconnectDelay = 5000;
注册 STOMP
通过重写 registerStompEndpoints 方法,进行注册端点,一种简单的消息协议。
通过 StompEndpointRegistry 的 addEndpoint方法,进行注册名为 websocket 端点。
通过 setAllowedOriginPatterns 设置允许跨域。* 表示允许所有。
通过 withSockJS 设置启动 SockJS 的支持,提供了对WebSocket API的抽象,使得在WebSocket不可用时,客户端仍然能够使用其他传输方式与服务器进行通信。
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket")
.setAllowedOriginPatterns("*")
.withSockJS();
}
配置认证
我们创建 WebSocketAuthorizationSecurityConfig 类继承 AbstractSecurityWebSocketMessageBrokerConfigurer 它具有 WebSocket安全配置的基本功能。
通过重写 configureInbound 方法,表示所有的 websocket 请求,都需要进行入站,通过重写 sameOriginDisabled 返回true表示禁用同源策略。
通过 设置 messages.anyMessage().authenticated();,表示所有的请求都需要认证
@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// 需要认证
messages.anyMessage().authenticated();
}
// 这里请自己按需求修改
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
通过重写 configureClientInboundChannel 方法,表示入站拦截器,其中我们自定义一个拦截器,从中取出 Token 获取用户,并设置身份信息。
其中我们重写拦截器的 preSend 方法,表示在发送请求之前拦截。
有两个参数,Message<?> 参数表示 WebSocket 消息,MessageChannel 参数表示消息发送的通道。
StompHeaderAccessor 类 用于访问STOMP消息的头部信息,比如命令(CONNECT、SEND、SUBSCRIBE等)和头部信息。
我们就从这个类中获取我们存入的 Token 令牌。
我们检查是否是连接信息,连接信息包含 accessor.getCommand() 的数据,如果为null就不是连接,直接放行即可。
如果是,进行认证, 通过 getFirstNativeHeader 方法获取我们指定的 Token 名,一般为 Authorization
最后重点是,我们创建 UsernamePasswordAuthenticationToken 将用户信息放进去,这样可以在 Security 中获取当前用户。 accessor.setUser(user) 一定要把 User 对象存入啊。后期直接通过 Security 中的作用域获取,最后返回 Message 就好啦。
不要忘记把创建的拦截器加入哦, registration.interceptors(interceptor);
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
ChannelInterceptor interceptor = new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
// 获取消息头访问器,用于处理STOMP消息的头信息
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
// 检查访问器和命令是否为空
if (accessor == null || accessor.getCommand() == null) {
return null; // 返回空表示继续处理下一个拦截器
}
// 处理CONNECT命令,用于用户认证
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// 从消息头中获取Authorization头(包含用户的身份认证token)
String token = accessor.getFirstNativeHeader("Authorization");
// 如果token为空,表示没有身份认证信息,返回空继续处理下一个拦截器
if (token == null) {
return null;
}
// 剥离token前缀,获取原始token
token = token.replace(Constants.TOKEN_PREFIX, "");
// 通过token服务获取LoginUser对象,用于创建用户Principal
LoginUser loginUser = tokenService.getLoginUser(token);
// 设置认证类
final UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken(
loginUser,
null,
loginUser.getAuthorities()
);
// 将用户Principal设置到访问器中,以便后续处理使用
accessor.setUser(user);
}
// 返回处理后的消息,继续后续处理
return message;
}
};
// 将拦截器注册到WebSocket消息处理的Channel
registration.interceptors(interceptor);
}
后期在控制器获取用户对象的方法。
@MessageMapping 和 requestMapping 一样,放在类上,表示一级路经,放在方法上,表示二级路径。
用于表示接收时的路径。
@SendTo 是返回到当前Message路径,是所有订阅用户 @SendToUser 是返回到当前Message路径,是当前用户,或者对方指定向自己发。
simpMessagingTemplate 是SpringWebsocket 提供的,用于主动推送信息给客户端。
simpMessagingTemplate.convertAndSend 表示向所有在线用户发送信息,参数1是订阅地址,参数2是数据。
@RestController
@MessageMapping("/cover/channel")
public class ChannelSocketController extends BaseController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/deptChannel")
@SendTo("/topic/deptChannel")
public AjaxResult getDeptByIdSocket(ManholeCover manholeCover) {
simpMessagingTemplate.convertAndSend("/topic/deptChannel", success);
return success;
}
}
前端 stomp 连接
创建 Stomp 客户端,设置ws地址,并链接,订阅user消息
安装
@stomp/stompjs
pnpm i sockjs-client
import SockJS from 'sockjs-client/dist/sockjs.min.js'
const sockJs = new SockJS('http://localhost:4646/websocket');
const client = Stomp.over(sockJs)
client.connect({"Authorization": 'Bearer ' + getToken()}, () => {
// 加载地图信息,是后端的MessageMapping路径
client.send('/app/cover/channel/deptChannel', {"Authorization": 'Bearer ' + getToken()}, JSON.stringify({}));
// 订阅消息,是后端的 @SendTo 路径,如果前缀加 user表示接收自己的,或者对方指定向自己发。
client.subscribe('/topic/deptChannel', (message) => {
console.log('Received222222:', message.body);
})
})
请 CV
前端 Stompjs 连接
import { Stomp } from "@stomp/stompjs" // 引入 Stomp.js
import { getToken } from "@/utils/auth.js" // 导入获取 token 的函数
// 定义仓库和客户端对象
let client = null
// 连接客户端函数
const connectClient = () => {
// 创建 Stomp 客户端对象,连接到指定的 WebSocket 服务器
client = Stomp.client('ws://localhost:4646/websocket')
// 配置心跳参数
client.heartbeat.outgoing = 10000 // 发送心跳间隔(毫秒)
client.heartbeat.incoming = 10000 // 接收心跳间隔(毫秒)
client.reconnectDelay = 5000 // 设置重新连接延迟(毫秒)
// 设置调试函数,用于捕获连接关闭事件
client.debug = (message) => {
// 开启日志打印
console.log(message)
// 如果消息以 'Connection closed' 开头,则表示连接已关闭
if (message.startsWith('Connection closed')) {
// 打印日志并执行相应逻辑
console.log('连接已关闭')
}
}
// 连接客户端到服务器,携带 token
client.connect({ Authorization: 'Bearer ' + getToken() }, () => {
// 订阅数据频道
client.subscribe('/topic/deptChannel', (message) => {
})
})
// 打印连接成功日志
console.log('连接成功')
}
// 导出获取客户端对象的函数
export const getClient = () => client;
// 导出连接客户端函数
export const ConnectClient = () => connectClient();
配置安全认证
/**
* WebSocket权限配置类,用于配置WebSocket消息的安全性。
*/
@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
/**
* 配置入站消息的安全性,使所有入站消息都需要经过身份验证。
*/
@Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// 添加自己的映射,要求所有入站消息都需要经过身份验证
messages.anyMessage().authenticated();
}
/**
* 配置是否禁用同源策略。根据实际需求返回是否禁用同源策略。
*/
@Override
protected boolean sameOriginDisabled() {
// 这里请自己按需求修改,返回是否禁用同源策略
return true;
}
}
后端配置 stompjs 协议
import static com.common.socket.StompUtils.ONLINE_USER;
/**
* WebSocket 配置类
*/
@Configuration
@EnableWebSocketMessageBroker
// 指定优先级,保证在Security配置之前生效
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private WebTokenService webTokenService;
/**
* 注册 Stomp 端点
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket")
// 设置允许跨域的域名
.setAllowedOriginPatterns("*")
// .withSockJS() // 如果需要使用 SockJS,可以添加该方法
;
}
/**
* 配置消息代理
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 设置客户端接收点对点消息地址的前缀,默认为 /user
registry.setUserDestinationPrefix("/user");
registry.setApplicationDestinationPrefixes("/app"); // 定义应用程序的前缀,客户端发送消息的目标前缀
// 设置定时器
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(3);
taskScheduler.setThreadNamePrefix("wss-heartbeat-thread-");
taskScheduler.initialize();
// 配置心跳
registry.enableSimpleBroker("/topic")
.setHeartbeatValue(new long[]{10000, 10000}) // 设置心跳间隔
.setTaskScheduler(taskScheduler); // 使用指定的任务调度器处理心跳任务
}
/**
* 配置客户端入站通道
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// 设置通道拦截器
ChannelInterceptor interceptor = new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
// 获取 Stomp 头部访问器
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor == null || accessor.getCommand() == null) return null;
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// 处理连接请求
String token = accessor.getFirstNativeHeader("Authorization");
if (token == null) return null;
// 解析 token,获取登录用户信息
token = token.replace(Constants.TOKEN_PREFIX, "");
LoginUser loginUser = webTokenService.getLoginUser(token);
// 将登录用户添加到在线用户列表
ONLINE_USER.put(loginUser.getUsername(), loginUser);
// 配置 Security 认证
final UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken(
loginUser,
null,
loginUser.getAuthorities()
);
// 设置 Stomp 消息头部的用户信息
accessor.setUser(user);
}
return message;
}
};
registration.interceptors(interceptor); // 添加拦截器
}
}
后端工具类,获取在线用户
public class StompUtils {
/*在线用户*/
public final static HashMap<String, LoginUser> ONLINE_USER = new HashMap<>();
/*根据用户Id,获取在线用户名*/
public static String getUserName(Long userId) {
return ONLINE_USER.entrySet().stream().filter(entry -> entry.getValue().getUserId().equals(userId))
.map(Map.Entry::getKey).findFirst().orElse(null);
}
}