You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Boot整合Keycloak认证的STOMP WebSocket通知推送异常排查

Spring Boot整合Keycloak认证的STOMP WebSocket通知推送异常排查

我最近在做一个Spring Boot项目,用STOMP over SockJS实现实时通知功能,同时集成了Keycloak做JWT认证。目前遇到个头疼的问题:后端明明已经触发了消息推送逻辑,前端却完全收不到通知。想请大伙帮忙看看哪里出问题了,先把当前的实现和情况列出来:

一、整体业务流程

  • 前端通过/ws端点发起WebSocket连接,在请求头里携带JWT的Authorization: Bearer <token>
  • 后端通过ChannelInterceptor拦截CONNECT请求,解析JWT后将用户的sub(UUID格式)设置为WebSocket会话的principal
  • 前端成功连接后,订阅/user/notifications主题
  • 后端业务逻辑触发通知时,调用SimpMessagingTemplate.convertAndSendToUser(userId, "/notifications", message)推送消息,这里的userId就是从Keycloak拿到的用户sub

二、核心代码配置

1. WebSocketConfig配置类

@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final JwtDecoder jwtDecoder;
    private final JwtAuthConverter jwtAuthConverter;

    @Value("${oxyreimobile.oauth2.keycloak.realm}")
    private String realm;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*") // TODO 后续调整为具体允许的源
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/user");
        registry.setApplicationDestinationPrefixes("/app");
        registry.setUserDestinationPrefix("/user");
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    String authHeader = accessor.getFirstNativeHeader("Authorization");
                    if (authHeader != null && authHeader.startsWith("Bearer ")) {
                        String token = authHeader.substring(7);
                        try {
                            Jwt jwt = jwtDecoder.decode(token);
                            Authentication authentication = jwtAuthConverter.convert(jwt);
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                            accessor.setUser(authentication);
                            System.out.println("✅ WebSocket CONNECT: Principal = " + authentication.getName());
                        } catch (Exception e) {
                            throw new RuntimeException("JWT non valido: " + e.getMessage());
                        }
                    }
                }
                return message;
            }
        });
    }
}

2. 后端推送逻辑

业务服务中调用推送的代码:

// 业务触发时调用,emailCaregiver是用户邮箱,先拿到sub再推
sendNotificationWebSocket(keycloakServices.getSub(emailCaregiver), n);

private void sendNotificationWebSocket(String userId, Notifications notifica) {
    System.out.println("📤 Invio WebSocket a utente: " + userId + " contenuto: " + notifica.getSubject());
    sim.convertAndSendToUser(
        userId, 
        "/notifications", 
        Map.of("content", "Nuova notifica: " + notifica.getSubject())
    );
}

3. 前端测试代码

我写了个简单的HTML页面测试:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test WebSocket</title>
</head>
<body>
<h1>Test WebSocket</h1>
<!-- SockJS -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
<!-- STOMP -->
<script src="https://cdn.jsdelivr.net/npm/stompjs/lib/stomp.min.js"></script>
<script>
    const socket = new SockJS('http://localhost:8080/ws');
    const client = Stomp.over(socket);
    const token = "这里替换成有效JWT";
    client.connect(
        { Authorization: `Bearer ${token}` }, 
        frame => {
            console.log('✅ Connesso al WebSocket:', frame);
            client.subscribe(`/user/notifications`, msg => {
                console.log("📩 Notifica ricevuta:", msg.body);
            });
        }, 
        error => console.error('❌ Errore connessione WebSocket:', error)
    );
</script>
</body>
</html>

三、后端关键日志

从日志里能看到关键环节都是正常的:

2025-09-19T23:29:26.755+02:00 DEBUG 10008 --- [app] [nio-8080-exec-5] s.w.s.h.LoggingWebSocketHandlerDecorator : New WebSocketServerSockJsSession[id=smnkzqoc]
✅ WebSocket CONNECT: Principal = 71412206-e72d-478c-b1f2-4447f386424d
2025-09-19T23:29:29.239+02:00 DEBUG 10008 --- [app] [nboundChannel-2] o.s.m.s.b.SimpleBrokerMessageHandler : Processing CONNECT session=smnkzqoc
2025-09-19T23:29:36.053+02:00 DEBUG 10008 --- [app] [nboundChannel-5] o.s.m.s.b.SimpleBrokerMessageHandler : Processing SUBSCRIBE /user/notifications id=sub-0 session=smnkzqoc
2025-09-19T23:29:43.693+02:00 INFO 10008 --- [app] [nio-8080-exec-2] c.m.o.o.s.impl.KeycloakServiceImpl : getSub of the username useruser@live.it
📤 Invio WebSocket a utente: 71412206-e72d-478c-b1f2-4447f386424d contenuto: E' presente un questionario da compilare, accedi all'apposita area per completare il <B>Questionnaire</B>
2025-09-19T23:29:43.920+02:00 DEBUG 10008 --- [app] [nio-80...

可以看到:

  • 连接时设置的principal是71412206-e72d-478c-b1f2-4447f386424d
  • 前端成功订阅了/user/notifications
  • 推送时用的userId和principal完全一致

四、已确认&疑问

✅ 已确认:

  • JWT解析正常,principal设置正确(日志打印的principal和用户sub一致)
  • 前端连接、订阅都成功,没有报错
  • 推送的userId和会话principal完全匹配

❓ 疑问:
明明所有环节看起来都正常,为什么前端就是收不到推送的消息?有没有大佬遇到过类似的问题,或者帮我看看代码配置里有没有隐藏的坑?

五、我自己琢磨的几个排查方向

  1. UserDestinationPrefix的映射逻辑:我配置了setUserDestinationPrefix("/user"),前端订阅/user/notifications,后端convertAndSendToUser会不会把目标转换成/user/71412206-e72d-478c-b1f2-4447f386424d/notifications?那前端订阅的是/user/notifications,是不是应该订阅/user/{userId}/notifications?不对啊,我记得Spring会自动处理用户目标的映射,前端只需要订阅/user/notifications就行?

  2. Authentication的name是否正确convertAndSendToUser默认是用Authentication.getName()来匹配WebSocket会话的,我的jwtAuthConverter是不是真的把JWT的sub设置成了Authentication的name?会不会转换器返回的name是其他字段(比如用户名)?

  3. SimpleBroker的配置:我用了enableSimpleBroker("/user"),是不是应该把/user下的所有子路径都代理?有没有可能SimpleBroker的配置不支持这种用户专属的目标转发?

  4. SockJS的兼容性:会不会是SockJS的帧处理有问题?比如消息已经推到前端,但SockJS没有正确转发到STOMP客户端?

希望大伙能给点思路,或者指出我代码里的明显错误,感谢!

火山引擎 最新活动