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

kotlin - Setup Spring OAuth based authenticationauthorization Using Web Sockets - Stack Overflow

programmeradmin0浏览0评论

I'm using .springframework.security:spring-security-messaging.

To start with I have a project that is successfully authenticating using oauth...

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ...
          audiences:
            - ...
@Configuration
@EnableWebSecurity
class SecurityConfig(
    @Autowired(required = false) private val requestLoggingFilter: RequestLoggingFilter?
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .oauth2ResourceServer { oauth2 ->
                oauth2.jwt { }
            }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/protected/**").authenticated()
                    .anyRequest().permitAll()
            }

        return http.build()
    }

The basic http JWT functions as expected. So I try to add a web socket example that I can also try...

@Configuration
@EnableWebSocketMessageBroker
class WebsocketConfig : WebSocketMessageBrokerConfigurer {

    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        config.enableSimpleBroker("/topic")
        config.setApplicationDestinationPrefixes("/app")
    }

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry.addEndpoint("/gs-guide-websocket")
    }
}

@Controller
class WebSocketController {
    private val logger = LoggerFactory.getLogger(WebSocketController::class.java)

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    fun greeting(message: TestMessage): Greeting {
        logger.info(message.toString())
        return Greeting("Hello, " + message.name + "!")
    }
    @MessageMapping("/status")
    @SendTo("/topic/chat")
    fun status(message: String): String {
        return "Status: $message"
    }
}

Now I am trying to add security so I add the following to the security config...

@Profile("secure")
@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig {
    @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("/app/status").permitAll()
            .simpDestMatchers("/app/hello").authenticated()
            .anyMessage().authenticated()
        return messages.build()
    }
}

When I try sending the non-protected message like

SEND
destination:/app/status
{"name":"test"}

I see that the message is working and being send, however, when I try to call with something like

SEND
destination:/app/hello
Authorization: Bearer ey...

{"name":"test"}

And I still get

.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]
    at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:149) ~[spring-messaging-6.2.2.jar:6.2.2]
    at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:125) ~[spring-messaging-6.2.2.jar:6.2.2]
    at .springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:343) ~[spring-websocket-6.2.2.jar:6.2.2]
...
Caused by: .springframework.security.access.AccessDeniedException: Access Denied
    at .springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]
    at .springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:181) ~[spring-messaging-6.2.2.jar:6.2.2]
    at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:135) ~[spring-messaging-6.2.2.jar:6.2.2]
    ... 44 common frames omitted

Same as with no header at all. The same token works fine on a regular web endpoint so what am I missing?

even when I add the following...

logging:
  level:
    .springframework.security: DEBUG
    .springframework.messaging: DEBUG
    .springframework.web.socket: DEBUG
    .springframework.security.oauth2: TRACE

I changed the endpoint to ws but not I see

2025-02-02T10:47:16.609-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] o.s.w.s.s.s.WebSocketHttpRequestHandler  : GET /ws
2025-02-02T10:47:16.641-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] s.w.s.h.LoggingWebSocketHandlerDecorator : New StandardWebSocketSession[id=e28c3083-e231-c00d-dc20-2cdb4d01ecac, uri=ws://.../ws]
2025-02-02T10:47:18.340-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Authorizing message send
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager .springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@5a4d7ce9 and result AuthorizationDecision [granted=false]
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] o.s.w.s.m.StompSubProtocolHandler        : Failed to send message to MessageChannel in session e28c3083-e231-c00d-dc20-2cdb4d01ecac

.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]

Also here is an example project that has the same issue if you want to play

I'm using .springframework.security:spring-security-messaging.

To start with I have a project that is successfully authenticating using oauth...

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ...
          audiences:
            - ...
@Configuration
@EnableWebSecurity
class SecurityConfig(
    @Autowired(required = false) private val requestLoggingFilter: RequestLoggingFilter?
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .oauth2ResourceServer { oauth2 ->
                oauth2.jwt { }
            }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/protected/**").authenticated()
                    .anyRequest().permitAll()
            }

        return http.build()
    }

The basic http JWT functions as expected. So I try to add a web socket example that I can also try...

@Configuration
@EnableWebSocketMessageBroker
class WebsocketConfig : WebSocketMessageBrokerConfigurer {

    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        config.enableSimpleBroker("/topic")
        config.setApplicationDestinationPrefixes("/app")
    }

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry.addEndpoint("/gs-guide-websocket")
    }
}

@Controller
class WebSocketController {
    private val logger = LoggerFactory.getLogger(WebSocketController::class.java)

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    fun greeting(message: TestMessage): Greeting {
        logger.info(message.toString())
        return Greeting("Hello, " + message.name + "!")
    }
    @MessageMapping("/status")
    @SendTo("/topic/chat")
    fun status(message: String): String {
        return "Status: $message"
    }
}

Now I am trying to add security so I add the following to the security config...

@Profile("secure")
@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig {
    @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("/app/status").permitAll()
            .simpDestMatchers("/app/hello").authenticated()
            .anyMessage().authenticated()
        return messages.build()
    }
}

When I try sending the non-protected message like

SEND
destination:/app/status
{"name":"test"}

I see that the message is working and being send, however, when I try to call with something like

SEND
destination:/app/hello
Authorization: Bearer ey...

{"name":"test"}

And I still get

.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]
    at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:149) ~[spring-messaging-6.2.2.jar:6.2.2]
    at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:125) ~[spring-messaging-6.2.2.jar:6.2.2]
    at .springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:343) ~[spring-websocket-6.2.2.jar:6.2.2]
...
Caused by: .springframework.security.access.AccessDeniedException: Access Denied
    at .springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]
    at .springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:181) ~[spring-messaging-6.2.2.jar:6.2.2]
    at .springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:135) ~[spring-messaging-6.2.2.jar:6.2.2]
    ... 44 common frames omitted

Same as with no header at all. The same token works fine on a regular web endpoint so what am I missing?

even when I add the following...

logging:
  level:
    .springframework.security: DEBUG
    .springframework.messaging: DEBUG
    .springframework.web.socket: DEBUG
    .springframework.security.oauth2: TRACE

I changed the endpoint to ws but not I see

2025-02-02T10:47:16.609-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] o.s.w.s.s.s.WebSocketHttpRequestHandler  : GET /ws
2025-02-02T10:47:16.641-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-3] s.w.s.h.LoggingWebSocketHandlerDecorator : New StandardWebSocketSession[id=e28c3083-e231-c00d-dc20-2cdb4d01ecac, uri=ws://.../ws]
2025-02-02T10:47:18.340-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Authorizing message send
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager .springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@5a4d7ce9 and result AuthorizationDecision [granted=false]
2025-02-02T10:47:18.341-05:00 DEBUG 88460 --- [love-monkey] [nio-7080-exec-4] o.s.w.s.m.StompSubProtocolHandler        : Failed to send message to MessageChannel in session e28c3083-e231-c00d-dc20-2cdb4d01ecac

.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]

Also here is an example project that has the same issue if you want to play

Share Improve this question edited Feb 5 at 13:35 Jackie asked Feb 1 at 18:09 JackieJackie 23.6k41 gold badges171 silver badges330 bronze badges 5
  • enable debug logging and read the real reason for your your access denied – Toerktumlare Commented Feb 2 at 12:10
  • Added your line @Toerktumlare don't see much useful but let me know if I missed something or if there is another package I should set – Jackie Commented Feb 2 at 15:51
  • WebSocket Security appears to be well documented: docs.spring.io/spring-security/reference/servlet/integrations/… Also have a look at the tests in the Spring Security source. – Roar S. Commented Feb 3 at 11:26
  • That is what I used and should map to what is there @RoarS. I thought I included the link in the question but thanks for pointing it out because I fot. But it still isn't working. – Jackie Commented Feb 3 at 13:50
  • The main sticking point I think is how do I debug why it is getting denied when there is a valid token that works as an http header? Is my message wrong somehow? – Jackie Commented Feb 3 at 14:06
Add a comment  | 

1 Answer 1

Reset to default 1

To help debug

Let's try putting code on top of this repo from OP and creating a test environment that uses fake bearer tokens.


CustomCsrfChannelInterceptor & WebSocketSecurityConfig

First make sure you have a custom csrf interceptor to avoid confusion. This was kind of tricky because csrf-header cannot be turned off, but I found a hack in this answer. Not for production use.

// https://stackoverflow/a/77057255/14072498
@Component("csrfChannelInterceptor")
class CustomCsrfChannelInterceptor : ChannelInterceptor {
    override fun preSend(
        message: Message<*>,
        channel: MessageChannel
    ): Message<*> {
        return message
    }
}

@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig {
    @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("/app/status").permitAll()
            .simpDestMatchers("/app/hello").authenticated()
            .anyMessage().authenticated()
        return messages.build()
    }
}

Also make a custom JWTDecoder to avoid having to use a real one. You can make this a TestConfig or a real config with a profile if you want to test in a dev environment, I chose the later.

@Configuration
@Profile("fake-jwt")
class FakeJwtConfig {
    @Bean
    fun jwtDecoder(): JwtDecoder {
        return JwtDecoder { token ->
            if (token != "test.token") {
                throw JwtException("Invalid token")
            }

            Jwt.withTokenValue(token)
                .header("alg", "none")
                .claim("sub", "user")
                .issuedAt(Instant.now())
                .expiresAt(Instant.now().plusSeconds(3600))
                .build()
        }
    }
}

WebSocketController

Let's log the Authentication in the controller

@Controller
class WebSocketController {
    private val logger = LoggerFactory.getLogger(WebSocketController::class.java)

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    fun greeting(message: TestMessage): Greeting {
        logger.info(message.toString())
        return Greeting("Hello, " + message.name + "!")
    }
    @MessageMapping("/status")
    @SendTo("/topic/chat")
    fun status(message: String): String {
        return "Status: $message"
    }
}

The Test

To ensure the issue isn't with Postman (since that can be complicated) lets create a few tests to make sure everything is working correctly

The test should pass showing that everything is working.

发布评论

评论列表(0)

  1. 暂无评论