最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Is there a way to notify a web socket if they are not permitted to subscribe using spring-security? - Stack Overflo

programmeradmin3浏览0评论

For the full repo look here

I have the following...

    @Bean
    fun messageAuthorizationManager(
        messages: MessageMatcherDelegatingAuthorizationManager.Builder
    ): AuthorizationManager<Message<*>> {
        messages
            // Next 2 lines are required for requests without auth.
            // Remove these if all paths require auth
            .simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
            .simpTypeMatchers(SimpMessageType.DISCONNECT).permitAll()
            .simpDestMatchers("/topic/greetings", "/app/hello").authenticated()
            .simpDestMatchers("/topic/status", "/app/status").permitAll()
            .anyMessage().authenticated()
        return messages.build()
    }
    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    fun greeting(message: TestMessage): Greeting {
        logger.info(message.toString())
        return Greeting("Hello, " + message.name + "!")
    }

    @MessageMapping("/status")
    @SendTo("/topic/status")
    fun status(message: String): String {
        return "Status: $message"
    }

However, when I try to consume these in JS like

    const subscribeToGreetings = useCallback(() => {
        if (!client || !client.active) return;

        setCanRetryGreetings(false);
        console.log('Attempting to subscribe to greetings...');
        client.subscribe('/topic/greetings',
            (message) => {
                console.log('Received greeting:', message);
                setMessages(prev => [...prev, `Greeting: ${message.body}`]);
            },
            {
                onError: (err) => {
                    console.error('Subscription error frame:', errmand, err.headers, err.body);
                    setMessages(prev => [...prev, `Permission denied: Cannot subscribe to greetings (${err.headers?.message || 'Unknown error'})`]);
                    setCanRetryGreetings(true);
                }
            }
        );
    }, [client]);

On the front end I just see

Attempting to subscribe to greetings...

I never actually see the onError part run. On the backend I do see....

2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@30c62bbb and result AuthorizationDecision [granted=false]
2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] o.s.w.s.m.StompSubProtocolHandler        : Failed to send message to MessageChannel in session da876e1d-f266-eb9f-08e4-8dc7e068b0fa
...
Caused by: org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]

So what am I missing why isn't the front end getting notified that their request to subscribe has failed?

For the full repo look here

I have the following...

    @Bean
    fun messageAuthorizationManager(
        messages: MessageMatcherDelegatingAuthorizationManager.Builder
    ): AuthorizationManager<Message<*>> {
        messages
            // Next 2 lines are required for requests without auth.
            // Remove these if all paths require auth
            .simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
            .simpTypeMatchers(SimpMessageType.DISCONNECT).permitAll()
            .simpDestMatchers("/topic/greetings", "/app/hello").authenticated()
            .simpDestMatchers("/topic/status", "/app/status").permitAll()
            .anyMessage().authenticated()
        return messages.build()
    }
    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    fun greeting(message: TestMessage): Greeting {
        logger.info(message.toString())
        return Greeting("Hello, " + message.name + "!")
    }

    @MessageMapping("/status")
    @SendTo("/topic/status")
    fun status(message: String): String {
        return "Status: $message"
    }

However, when I try to consume these in JS like

    const subscribeToGreetings = useCallback(() => {
        if (!client || !client.active) return;

        setCanRetryGreetings(false);
        console.log('Attempting to subscribe to greetings...');
        client.subscribe('/topic/greetings',
            (message) => {
                console.log('Received greeting:', message);
                setMessages(prev => [...prev, `Greeting: ${message.body}`]);
            },
            {
                onError: (err) => {
                    console.error('Subscription error frame:', err.command, err.headers, err.body);
                    setMessages(prev => [...prev, `Permission denied: Cannot subscribe to greetings (${err.headers?.message || 'Unknown error'})`]);
                    setCanRetryGreetings(true);
                }
            }
        );
    }, [client]);

On the front end I just see

Attempting to subscribe to greetings...

I never actually see the onError part run. On the backend I do see....

2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@30c62bbb and result AuthorizationDecision [granted=false]
2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] o.s.w.s.m.StompSubProtocolHandler        : Failed to send message to MessageChannel in session da876e1d-f266-eb9f-08e4-8dc7e068b0fa
...
Caused by: org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]

So what am I missing why isn't the front end getting notified that their request to subscribe has failed?

Share Improve this question asked Feb 7 at 18:59 JackieJackie 23.5k41 gold badges169 silver badges327 bronze badges 3
  • Hi @Jackie. Does .simpTypeMatchers(SimpMessageType.SUBSCRIBE).permitAll() make any change? – Roar S. Commented Feb 7 at 20:15
  • I can try but I want to make sure they can't subscribe right? The more I think about it the more I think it is expected behavior because what happens if you can't subscribe to any ws endpoints how could it communicate state back? – Jackie Commented Feb 7 at 20:45
  • You have an error in your code. I tested with the component in my answer, and that seems to work as expected. – Roar S. Commented Feb 8 at 17:21
Add a comment  | 

1 Answer 1

Reset to default 0

The problem here is that your component code is not correct. Third parameter for subscribe is StompHeaders, not an error handler.

Error handler should be registered with your Client, see onStompError below.

Please see below code for a functioning React component. It is written with TypeScript, but it should be easy to remove types.

Tested with @stomp/stompjs 7.0.0.

import {useEffect, useState} from "react"
import {Client} from "@stomp/stompjs"

const StompTestComponent = () => {
    const [messages, setMessages] = useState<string[]>([])
    const [client, setClient] = useState<Client | null>(null)

    useEffect(() => {
        const stompClient = new Client({
            brokerURL: "ws://localhost:8080/ws",
            onConnect: () => console.log("STOMP client connected"),
            onDisconnect: () => console.log("STOMP client disconnected"),
            onStompError: frame => console.error("STOMP client error:", frame),
        })
        setClient(stompClient)
        stompClient.activate()

        // clean-up
        return () => {
            if (stompClient && stompClient.active) {
                stompClient.deactivate().then(() => console.log("STOMP client deactivated"))
            }
        }
    }, [])

    const subscribeToGreetings = () => {
        if (!(client && client.active)) return

        console.log("Attempting to subscribe to greetings...")

        client.subscribe(
            "/topic/greetings",
            message => {
                console.log("Received greeting:", message)
                setMessages(prev => [...prev, `Greeting: ${message.body}`])
            }
        )
    }

    if (!client) {
        return <div>Initializing...</div>
    }

    return <div>
        <button onClick={subscribeToGreetings}>Subscribe to Greetings</button>
        <div>
            {messages.map((message, idx) => <div key={idx}>{message}</div>)}
        </div>
    </div>
}

export default StompTestComponent

If you want custom error details, you can implement a custom StompSubProtocolErrorHandler in your Spring Boot app like this

import org.springframework.security.access.AccessDeniedException
import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler


class CustomStompSubProtocolErrorHandler : StompSubProtocolErrorHandler() {
    override fun handleClientMessageProcessingError(
        clientMessage: Message<ByteArray>?,
        ex: Throwable
    ): Message<ByteArray> {
        val exception =
            if (ex is MessageDeliveryException) ex.cause ?: ex
            else ex

        val errorCode = when (exception) {
            is AccessDeniedException -> "ACCESS_DENIED"
            else -> "UNKNOWN_ERROR"
        }

        val errorMessage = when (exception) {
            is AccessDeniedException -> "You do not have permission to access this resource."
            else -> exception.message ?: "An unexpected error occurred."
        }

        return createErrorMessage(errorMessage, errorCode)
    }

    companion object {
        private fun createErrorMessage(
            errorMessage: String,
            errorCode: String
        ): Message<ByteArray> {
            val headerAccessor = StompHeaderAccessor.create(StompCommand.ERROR).apply {
                message = errorCode
            }

            return MessageBuilder.createMessage(
                errorMessage.toByteArray(),
                headerAccessor.messageHeaders
            )
        }
    }
}

and register it like this

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry
            .addEndpoint("/ws")
            .setAllowedOriginPatterns("*")

        registry.setErrorHandler(CustomStompSubProtocolErrorHandler())
    }

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论