likes
comments
collection
share

SpringWebsocket 整合认证实战

作者站长头像
站长
· 阅读数 3

SpringWebsocket 提供了支持 Websocket 协议的模块,允许客户端和服务端进行实时操作。

它支持 Stomp 支持和 SockJS。

SpringWebSocket 配置

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</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");
}

注册 STOMP

通过重写 registerStompEndpoints 方法,进行注册端点,一种简单的消息协议。

通过 StompEndpointRegistry 的 addEndpoint方法,进行注册名为 websocket 端点。

通过 setAllowedOriginPatterns 设置允许跨域。* 表示允许所有。

通过 withSockJS 设置启动 SockJS 的支持,提供了对WebSocket API的抽象,使得在WebSocket不可用时,客户端仍然能够使用其他传输方式与服务器进行通信。

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/websocket")
            .setAllowedOriginPatterns("*")
            .withSockJS();
}

配置认证

通过重写 configureClientInboundChannel 方法,表示入站拦截器,其中我们自定义一个拦截器,从中取出 Token 获取用户,并设置身份信息。

其中我们重写拦截器的 preSend 方法,表示在发送请求之前拦截。

有两个参数,Message<?> 参数表示 WebSocket 消息,MessageChannel 参数表示消息发送的通道。

StompHeaderAccessor 类 用于访问STOMP消息的头部信息,比如命令(CONNECT、SEND、SUBSCRIBE等)和头部信息。

我们就从这个类中获取我们存入的 Token 令牌。

我们检查是否是连接信息,连接信息包含 accessor.getCommand() 的数据,如果为null就不是连接,直接放行即可。

如果是,进行认证, 通过 getFirstNativeHeader 方法获取我们指定的 Token 名,一般为 Authorization

最后重点是, accessor.setUser(userPrincipal) 一定要把 User 对象存入啊。后期直接获取,最后返回 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);
                UserPrincipal userPrincipal = new UserPrincipal(loginUser);
                
                // 将用户Principal设置到访问器中,以便后续处理使用
                accessor.setUser(userPrincipal);
            }

            // 返回处理后的消息,继续后续处理
            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, UserPrincipal userPrincipal) {
        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);
    })
})