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

如何为已实现的WebSocket聊天系统添加JWT认证?

Got it, let's walk through implementing JWT authentication for your WebSocket chat project. I've built similar setups with Spring WebSocket + Spring Security, so here's a practical, step-by-step approach that covers all your requirements:

1. Enforce JWT Authentication During WebSocket Handshake

WebSocket connections start with an HTTP handshake, so we can hook into that step to validate the JWT before allowing the connection to upgrade. We'll use a HandshakeInterceptor to check the token and attach the authenticated user ID to the WebSocket session.

First, create the handshake interceptor:

@Component
public class JwtHandshakeInterceptor implements HandshakeInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtHandshakeInterceptor(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        // Extract JWT from Authorization header
        String token = request.getHeaders().getFirst("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            try {
                // Validate token and get user ID
                String userId = jwtTokenProvider.getUserIdFromToken(token);
                if (userId != null) {
                    // Store user ID in session attributes for later use
                    attributes.put("userId", userId);
                    return true;
                }
            } catch (JwtException e) {
                // Invalid token: reject handshake
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return false;
            }
        }
        // No valid token: reject handshake
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return false;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        // No action needed here
    }
}

Then register this interceptor in your WebSocket config:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final JwtHandshakeInterceptor jwtHandshakeInterceptor;
    private final JwtChannelInterceptor jwtChannelInterceptor;

    public WebSocketConfig(JwtHandshakeInterceptor jwtHandshakeInterceptor, JwtChannelInterceptor jwtChannelInterceptor) {
        this.jwtHandshakeInterceptor = jwtHandshakeInterceptor;
        this.jwtChannelInterceptor = jwtChannelInterceptor;
    }

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chatting")
                .addInterceptors(jwtHandshakeInterceptor)
                .setAllowedOrigins("https://your-frontend-domain.com"); // Replace with your actual frontend origin
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(jwtChannelInterceptor);
    }
}

Note: Replace JwtTokenProvider with your existing JWT utility class that handles token parsing, signature validation, and extracting the user ID.

2. Restrict Subscription to User's Own Inbox

We need to make sure users can only subscribe to /user/{userId}/queue/chatting where userId matches their authenticated ID. Use a ChannelInterceptor to check subscription destinations before allowing them.

Create the channel interceptor:

@Component
public class JwtChannelInterceptor implements ChannelInterceptor {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        
        // Check if the message is a SUBSCRIBE command
        if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
            String destination = accessor.getDestination();
            String currentUserId = (String) accessor.getSessionAttributes().get("userId");
            
            // Parse the target user ID from the subscription destination
            Pattern inboxPattern = Pattern.compile("/user/(.*)/queue/chatting");
            Matcher matcher = inboxPattern.matcher(destination);
            
            if (matcher.matches()) {
                String targetUserId = matcher.group(1);
                if (!currentUserId.equals(targetUserId)) {
                    throw new AccessDeniedException("You cannot subscribe to another user's inbox");
                }
            }
        }
        return message;
    }
}
3. Validate from Field Matches Authenticated User

When a user sends a message via /app/message, we need to ensure the from field in the payload matches their authenticated user ID. Add this check in your message controller:

@Controller
public class ChatController {

    private final SimpMessagingTemplate simpMessagingTemplate;

    public ChatController(SimpMessagingTemplate simpMessagingTemplate) {
        this.simpMessagingTemplate = simpMessagingTemplate;
    }

    @MessageMapping("/message")
    public void handleMessage(@Payload ChatMessage chatMessage, StompHeaderAccessor accessor) {
        String currentUserId = (String) accessor.getSessionAttributes().get("userId");
        
        // Validate that the 'from' field matches the authenticated user
        if (!currentUserId.equals(chatMessage.getFrom())) {
            throw new AccessDeniedException("The 'from' field does not match your authenticated identity");
        }
        
        // Forward the message to the recipient's inbox
        simpMessagingTemplate.convertAndSendToUser(
            chatMessage.getTo(),
            "/queue/chatting",
            chatMessage
        );
    }

    // DTO for chat messages
    public static class ChatMessage {
        private String message;
        private String from;
        private String to;

        // Getters and setters
        public String getMessage() { return message; }
        public void setMessage(String message) { this.message = message; }
        public String getFrom() { return from; }
        public void setFrom(String from) { this.from = from; }
        public String getTo() { return to; }
        public void setTo(String to) { this.to = to; }
    }
}
Additional Notes
  • Client Side: When connecting via Stomp.js, make sure to include the JWT in the handshake headers:
    const socket = new SockJS('/chatting');
    const stompClient = Stomp.over(socket);
    
    stompClient.connect(
      { Authorization: `Bearer ${yourJwtToken}` },
      (frame) => {
        // Subscribe to your own inbox after successful connection
        stompClient.subscribe(`/user/${yourUserId}/queue/chatting`, (message) => {
          // Handle incoming messages
          const chatMsg = JSON.parse(message.body);
          console.log("Received message:", chatMsg);
        });
      },
      (error) => {
        console.error("Connection failed:", error);
      }
    );
    
  • Error Handling: Add a custom exception handler for WebSocket to return user-friendly error messages instead of generic 500s.
  • Production Security: Avoid using setAllowedOrigins("*"); specify your exact frontend domain(s) to prevent CSRF risks.

内容的提问来源于stack exchange,提问作者Sergio Barbero

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 08:47:37