admin管理员组

文章数量:1277314

I have spring security with JWT implemented in my application and was trying to set up the WebSocket connection. Initially I was sending the token in the headers of the WebSocket connection with the stomp client, however I realized that spring security seems to handle authentication differently compared to HTTP requests.I then tried to test to see if I was able to get a connection up and running as I set permit All on the WebSocket connection endpoint in the filter chain, having done this I still get a 403 forbidden error. Currently if my spring security filter chain permits the handshake connection the connection should be successful considering I already set up spring security CORS configuration to accept my client as the origin. If I were to ensure WebSocket connections are permitted to users but the sending of messages requires authentication, would it mean I need to implement a custom message interceptor to validate my token for each message or can i just extend the AbstractSecurity WebSocketMessageBroker Configurer and ensure all endpoints with /app** are authenticated.

How would I authenticate subsequent requests under the STOMP protocol? Considering the Initial WebSocket connection is sent with under a HTTP protocol, even if I was to include the Authorization header then shouldn't it automatically be used by my JWT filter?

from the little documentation out there, what I've gathered is that if you include the token during the WebSocket handshake then because its under a HTTP protocol, your server will be able to validate the token. This would then set up the principal such that you can now set up subsequent messages to be authenticated by implementing the configureInbound method. However if I permit the handshake to not require authentication then I would need to implement a custom message interceptor that extracts the token from the send function on the the client side. something like

this.stompClient.send("/app/chat/send", {"Authorization" : "Bearer " + this.token}, JSON.stringify(message))

Below is all the code




import .springframework.context.annotation.Configuration;
import .springframework.messaging.simp.config.MessageBrokerRegistry;
import .springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import .springframework.web.socket.config.annotation.StompEndpointRegistry;
import .springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

     /// this method initiates the web socket connection, when a client wants to upgrade their
    /// protocol from HTTP to WebSocket
        /// . we also define the servers that can make initiate a websocket connection
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {

        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("http://localhost:49322")
                .withSockJS();
    }

    /// we define the prefix of the placeholder in the url as 'app' in which this will be binded to the
    /// MessageMapping annotation methods, similar to how requestMapping routed the endpoint to the specific method
    /// the message broker is used to define the endpoint in which a user will be subscribed to
    /// we set the user as the prefix
    ///
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/user");
        registry.setApplicationDestinationPrefixes("/app");
    }
}


package com.example.Dormly.websocket;

import lombok.RequiredArgsConstructor;
import .springframework.messaging.handler.annotation.MessageMapping;
import .springframework.messaging.handler.annotation.Payload;
import .springframework.messaging.simp.SimpMessagingTemplate;
import .springframework.security.core.annotation.AuthenticationPrincipal;
import .springframework.security.core.userdetails.UserDetails;

@.springframework.stereotype.Controller
@RequiredArgsConstructor
public class Controller {

    private final SimpMessagingTemplate simpMessagingTemplate;
    ///
    /// The @MessageMapping is used to route all /app/placeholder destinations to their specific methods
    /// we use the principal to define the user who is the sender, and via a UI action
    /// we also include recipient to define who the end user is
    /// we create an output message object which contains a sender and the content
    /// we then send this output message to the user
    /// The message object just has the recipient(to whom we send to) and the content, the sender is fetched from the principal
    @MessageMapping("/chat/send")
    public void sendMessage(@Payload  Message message , @AuthenticationPrincipal UserDetails user){
        OutputMessage outputMessage = new OutputMessage(
                message.getContent(),
                user.getUsername() /// user who sent the message - the recipient will see this
        );
        /// the server will send back something like '/user/james/queue/chat'
        simpMessagingTemplate.convertAndSendToUser(message.getRecipient(),"/queue/chat", outputMessage);


    }
}

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {

    private final AuthenticationProvider authenticationProvider;
    private final JwtAuthFilter jwtAuthFilter;


    /**
     * Configuration annotation tells spring that there is more than one bean that needs to be instantiated as a singleton
     * SecurityFilterChain applies a set of filters to our HTTP requests.
     */

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.
        cors(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(http->http.
                        requestMatchers("/api/v1/Sign-up").permitAll()
                        .requestMatchers("/api/v1/login").permitAll()
                        .requestMatchers("/ws**").permitAll()
                        .anyRequest()
                        .authenticated()
                )
        /**
         * ensure our session management remains stateless, as we authenticate once per request
         */

                        .sessionManagement(session-> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                )
                /**
                 * when spring security intercepts the login request, the usernamepasswordfilter delegates
                 * this to the auth manager in which that uses the provider manager impl to find the auth provider.
                 * The auth provider is called to validate the credentials as it receives an auth object
                 * Also ensure the jwt filter gets called before the usernamepass filter
                 * as we always need to check if a JWT is present, if not the request is passed to other filters
                 * This way we handle requests for unauthenticated and authenticated users
                 * not authenticated(meaning no jwt) -> UsernamePassFilter gets used
                 * authenticated(has Jwt) -> jwtAuthFilter
                 */

                .authenticationProvider(authenticationProvider)
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

                return httpSecurity.build();


    }

import { Injectable } from '@angular/core';
import { TokenService } from '../auth/token/token.service';
import SockJS from 'sockjs-client';
import { Stomp } from '@stomp/stompjs';
import { Message } from '../models/Message';
import { MessagesComponent } from '../messages/messagesponent';
import { BehaviorSubject } from 'rxjs';




@Injectable({
  providedIn: 'root'
})
export class WebSocketApiService {
  //create a subject to communicate the responses back to the messages component
  private subject = new BehaviorSubject(null)
  messageSubscription$ = this.subject.asObservable()


  destination:string = "/user/queue/chat"
  brokerURL:string = "http://localhost:8099/ws"
  stompClient:any
  token!:string

  constructor(private tokenService:TokenService) {
    this.token = this.tokenService.token as string
  }
  connect(){
    console.log("connecting to websocket...")
    const headers = { "Authorization": "Bearer " + this.token };
    console.log("WebSocket headers:", headers); // Log the headers

    let ws = new SockJS(this.brokerURL)
    this.stompClient = Stomp.over(ws)
    this.stompClient.connect({"Authorization" : `Bearer ${this.token}`}, () =>{
      console.log("WebSocket connected");

        ///the stompclient takes 3 parameters which includes the headers and 2 callback functions,
        ///the frames defined the handshake agreement of protocol switches, and once the event occurs we can now subscribe to the destination

        this.stompClient.subscribe(this.destination, (message:any)=>{
          //the subscribe also triggers a callback which means when a user subscribes to a destination, an event of a message could be returned
          this.onMessageRecieved(message)

        })

    },
    ///error callback
    (error:Error | any)=>{
    this.errorCallBack(error)
    }
  )



  }
  errorCallBack(error:Error):void{
    console.log(error.message)
    
  }

  disconnect(){
    if(this.stompClient!==null){
      this.stompClient.disconnect()
      console.log("disconnected")
    }
    setTimeout(()=>{
      this.connect()
    },
    5000) //reconnect after 5 seconds
    
  }


  onMessageRecieved(message:any) {
    if(message){
    ///manual deserialization with websockets. using Json.parse() to convert the json into a javascript objecy
    console.log("message recieved ", message)
    this.subject.next(JSON.parse(message))
    console.log("added message to the subject")
    }
 

  }


  send(message:Message):void{
    ///when the user sends a message, our in memory message broker in spring will automatically forward it to the destination the user is subscribed to
    ///the user will recieve the message immediatley assuming the connection is still live - if not we persist the chat to the db
    ///all messages are sent with the /app prefix and users are able to send messages
    ///the message object includes the recieptent and the message itself.
    ///websocket does not serialize the object into a json like HTTP does, hence we do it manually
    this.stompClient.send("/app/chat/send", {"Authorization" : "Bearer " + this.token}, JSON.stringify(message))
  }


}

This is what shows up in the console in order

[Log] calling service class to connect to websocket (main.js, line 2372)

[Log] connecting to websocket... (main.js, line 2275) [Warning] Stomp.over did not receive a factory, auto reconnect will not work. Please see docs/latest/classes/Stomp.html#over (@stomp_stompjs.js, line 1534)

[Log] Opening Web Socket... (@stomp_stompjs.js, line 1286) [Debug] [vite] connected. (client, line 859)

[Error] Failed to load resource: the server responded with a status of 403 () (info, line 0)

[Log] Connection closed to http://localhost:8099/ws (@stomp_stompjs.js, line 1286)

本文标签: java403 forbidden using websockets with spring bootStack Overflow