如何为@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

