You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为@stomp/stompjs或原生JS WebSocket添加Bearer Token授权头以对接Spring Boot OAuth认证的WebSocket服务

如何为@stomp/stompjs或原生JS WebSocket添加Bearer Token授权头以对接Spring Boot OAuth认证的WebSocket服务

兄弟,你这个问题我之前也碰到过!其实STOMP客户端传递Bearer Token的方式和Postman里的HTTP请求有点不一样,不过@stomp/stompjs是支持的,而且有两种常用的方式,结合你的Spring Boot OAuth配置来选就行,我给你一步步说清楚:

一、先搞懂核心问题:WebSocket/STOMP的授权逻辑

你Postman里用的是WebSocket握手阶段的HTTP请求头传递Authorization,这是可行的,但原生JS的WebSocket API不支持直接设置自定义握手头(浏览器限制),不过我们有两种替代方案:要么用SockJS(它的握手是HTTP请求,支持加头),要么在STOMP的CONNECT帧里携带授权信息,Spring Security都能适配。

另外你提到订阅时没有Authorization头、authorizationStrategy.isGranted返回false,这是正常的——WebSocket是长连接,认证只需要在连接建立阶段完成,后续的SUBSCRIBE、SEND等帧会复用连接的认证信息,问题出在你后端没正确提取连接阶段的认证信息,导致后续操作拿不到Authentication对象。


二、方案1:用SockJS在握手HTTP请求头加Authorization(和Postman完全一致)

SockJS会把WebSocket降级成HTTP请求做握手,所以可以像Postman一样加Authorization头,配置方式如下:

前端@stomp/stompjs代码

import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';

const WebSocketClient = () => {
  // 假设你从本地存储或接口拿到了Bearer Token
  const token = localStorage.getItem('access_token');
  const brokerURL = 'http://localhost:8080/ws'; // 你的SockJS端点(后端配置的)

  const connect = () => {
    const stompClient = new Client({
      // 用SockJS替代原生WebSocket,支持设置握手HTTP头
      webSocketFactory: () => {
        const sock = new SockJS(brokerURL);
        // 这里加的就是和Postman一样的Authorization头
        sock.setHeader('Authorization', `Bearer ${token}`);
        return sock;
      },
      onConnect: () => {
        setConnected(true);
        setError('');
        setCanRetryGreetings(true);

        // 订阅主题
        stompClient.subscribe('/topic/greeting', (message) => {
          setMessages(prev => [...prev, `Status: ${message.body}`]);
        });

        // 发送消息
        stompClient.publish({
          destination: '/app/hello',
          body: 'Client connected'
        });
      },
      onDisconnect: () => {
        setConnected(false);
        setCanRetryGreetings(true);
      },
      onError: (err) => {
        setError(`Connection error: ${err.message}`);
      }
    });

    stompClient.activate(); // 启动连接
  };

  // 你的UI渲染逻辑...
};

后端Spring Boot配置(适配HTTP握手头)

需要确保Spring Security能从WebSocket握手的HTTP头里提取Token,生成Authentication对象:

@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {

    @Autowired
    private JwtDecoder jwtDecoder;

    @Autowired
    private JwtAuthenticationConverter jwtAuthenticationConverter;

    // 配置WebSocket接口的权限规则
    @Bean
    public SecurityWebSocketMessageBrokerConfigurer securityWebSocketMessageBrokerConfigurer() {
        return (messages, registry) -> messages
                .simpDestMatchers("/topic/**", "/app/**").authenticated()
                .anyMessage().denyAll();
    }

    // 拦截STOMP帧,从握手HTTP头提取Token生成认证信息
    @Bean
    public ChannelInterceptor authenticationChannelInterceptor() {
        return new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                assert accessor != null;

                // 从握手的HTTP头获取Authorization
                HttpHeaders handshakeHeaders = (HttpHeaders) accessor.getHeader(SimpMessageHeaderAccessor.NATIVE_HEADERS);
                if (handshakeHeaders != null) {
                    String authHeader = handshakeHeaders.getFirst(HttpHeaders.AUTHORIZATION);
                    if (authHeader != null && authHeader.startsWith("Bearer ")) {
                        String token = authHeader.substring(7);
                        Jwt jwt = jwtDecoder.decode(token);
                        Authentication authentication = new JwtAuthenticationToken(jwt, jwtAuthenticationConverter.convert(jwt));
                        accessor.setUser(authentication); // 把认证信息绑定到当前连接
                    }
                }
                return message;
            }
        };
    }

    // 注册拦截器
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(authenticationChannelInterceptor());
    }
}

三、方案2:在STOMP CONNECT帧里传递Authorization头

如果不想用SockJS,直接用原生WebSocket,可以在STOMP的CONNECT帧里携带授权信息,这是STOMP协议标准的做法:

前端@stomp/stompjs代码

只需要在Client配置里加connectHeaders即可:

import { Client } from '@stomp/stompjs';

const WebSocketClient = () => {
  const token = localStorage.getItem('access_token');
  const brokerURL = 'ws://localhost:8080/ws'; // 原生WebSocket端点

  const connect = () => {
    const stompClient = new Client({
      brokerURL,
      // 把Authorization放在STOMP的CONNECT帧头里
      connectHeaders: {
        Authorization: `Bearer ${token}`
      },
      onConnect: () => {
        // 你的现有逻辑:订阅主题、发送消息
        setConnected(true);
        stompClient.subscribe('/topic/greeting', (message) => {
          setMessages(prev => [...prev, `Status: ${message.body}`]);
        });
        stompClient.publish({
          destination: '/app/hello',
          body: 'Client connected'
        });
      },
      // 其他回调配置...
      onError: (err) => {
        setError(`Connection error: ${err.message}`);
      }
    });

    stompClient.activate();
  };

  // 你的UI渲染逻辑...
};

后端Spring Boot配置(适配STOMP CONNECT帧)

只需要修改拦截器的Token提取逻辑,从STOMP帧头里拿:

@Bean
public ChannelInterceptor authenticationChannelInterceptor() {
    return new ChannelInterceptor() {
        @Override
        public Message<?> preSend(Message<?> message, MessageChannel channel) {
            StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
            assert accessor != null;

            // 从STOMP CONNECT帧头获取Authorization
            if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                String authHeader = accessor.getFirstNativeHeader("Authorization");
                if (authHeader != null && authHeader.startsWith("Bearer ")) {
                    String token = authHeader.substring(7);
                    Jwt jwt = jwtDecoder.decode(token);
                    Authentication authentication = new JwtAuthenticationToken(jwt, jwtAuthenticationConverter.convert(jwt));
                    accessor.setUser(authentication);
                }
            }
            return message;
        }
    };
}

四、解决你遇到的authorizationStrategy.isGranted返回false的问题

这个问题本质是后端没把认证信息绑定到WebSocket连接,只要你按上面的配置,在拦截器里把生成的Authentication对象通过accessor.setUser(authentication)绑定到连接上,后续的SUBSCRIBE等操作就能拿到认证信息,authorizationStrategy.isGranted就会返回true了。

另外你看到订阅时没有Authorization头是正常的——WebSocket是长连接,认证只在连接建立阶段做一次,后续所有操作都复用这个连接的认证状态,不需要重复传头。


备注:内容来源于stack exchange,提问作者Jackie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 09:38:05