如何为Spring Boot WebSocket端点编写安全性测试

huangapple go评论70阅读模式
英文:

How can I write Security tests for Spring Boot WebSocket endpoints

问题

以下是翻译好的内容:

我正在寻找一种明智的方法来单元测试我们的WebSocket安全实现。我们使用Spring的TestRestTemplate来测试我们的REST端点,似乎这种方法几乎适用于WebSockets。如果您接受将BAD_REQUEST用作OK的替代。

有没有一种好的方法来测试Spring WebSockets呢?

这是我们的处理程序映射:

@Configuration
class HandlerMappingConfiguration {
    @Bean
    fun webSocketMapping(webSocketHandler: DefaultWebSocketHandler?): HandlerMapping {
        val map: MutableMap<String, WebSocketHandler?> = HashMap()
        map["/"] = webSocketHandler
        map["/protected-ws"] = webSocketHandler

        val handlerMapping = SimpleUrlHandlerMapping()
        handlerMapping.order = 1
        handlerMapping.urlMap = map
        return handlerMapping
    }
}

这是我们的安全配置:

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class SecurityConfig(
    private val authenticationConverter: ServerAuthenticationConverter,
    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") private val issuerUri: String,
    @Value("${spring.security.oauth2.resourceserver.jwt.audience}") private val audience: String
) {
    // ...
}

这是我们用于REST端点的IntegrationTest类:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestSecurityTest(@Autowired val restTemplate: TestRestTemplate) {
    // ...
}

我们希望为WebSocket端点实现类似的功能... 例如:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebSocketSecurityTest(@Autowired val restTemplate: TestRestTemplate) {
    // ...
}

但这有一些限制:

  1. 我们的OK测试必须检查BAD_REQUEST响应!
  2. 我们无法验证来自端点的任何实际负载。

因此,它只适用于失败场景。此示例的完整源代码可在GitHub上找到

英文:

I'm looking for a sensible way to unit test our WebSocket security implementation. We use Spring TestRestTemplate to test our REST endpoints, it seems this approach almost works with WebSockets. If you accept BAD_REQUEST as a substitue for OK.

Is there a good way to test Spring WebSockets?

This is our handler mapping

@Configuration
class HandlerMappingConfiguration {
    @Bean
    fun webSocketMapping(webSocketHandler: DefaultWebSocketHandler?): HandlerMapping {
        val map: MutableMap&lt;String, WebSocketHandler?&gt; = HashMap()
        map[&quot;/&quot;] = webSocketHandler
        map[&quot;/protected-ws&quot;] = webSocketHandler

        val handlerMapping = SimpleUrlHandlerMapping()
        handlerMapping.order = 1
        handlerMapping.urlMap = map
        return handlerMapping
    }
}

This is our security configuration

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class SecurityConfig(
    private val authenticationConverter: ServerAuthenticationConverter,
    @param:Value(&quot;${spring.security.oauth2.resourceserver.jwt.issuer-uri}&quot;) private val issuerUri: String,
    @param:Value(&quot;${spring.security.oauth2.resourceserver.jwt.audience}&quot;) private val audience: String
) {
    @Bean
    fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http
            .csrf().disable()
            .authorizeExchange()
            .pathMatchers(&quot;/protected-ws&quot;).hasAuthority(&quot;SCOPE_read:client-ws&quot;)
            .pathMatchers(&quot;/protected&quot;).hasAuthority(&quot;SCOPE_read:client&quot;)
            .matchers(EndpointRequest.toAnyEndpoint()).permitAll()
            .anyExchange().authenticated()
            .and()
            .oauth2ResourceServer()
            .bearerTokenConverter(authenticationConverter)
            .jwt()
        return http.build()
    }

    @Bean
    open fun jwtDecoder(): ReactiveJwtDecoder {
        val jwtDecoder = ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
        jwtDecoder.setJwtValidator(DelegatingOAuth2TokenValidator(withIssuer(), audienceValidator()))
        return jwtDecoder
    }

    private fun withIssuer(): OAuth2TokenValidator&lt;Jwt&gt;? {
        return JwtValidators.createDefaultWithIssuer(
            issuerUri
        )
    }

    private fun audienceValidator(): OAuth2TokenValidator&lt;Jwt&gt; {
        return JwtAudienceValidator(
            audience
        )
    }
}

This is our IntegrationTest class for REST endpoints

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestSecurityTest(@Autowired val restTemplate: TestRestTemplate) {

    @Test
    fun `Unauthorized user is unauthorized`() {
        val entity = restTemplate.getForEntity&lt;String&gt;(&quot;/world&quot;, String::class.java)
        assertThat(entity.statusCode).isEqualTo(HttpStatus.UNAUTHORIZED)
    }

    @Test
    @WithMockUser(authorities = [&quot;SCOPE_read:world&quot;])
    fun `Authorized user, without required scope is Forbidden`() {
        val entity = restTemplate.getForEntity&lt;String&gt;(&quot;/protected&quot;, String::class.java)
        assertThat(entity.statusCode).isEqualTo(HttpStatus.FORBIDDEN)
    }

    @Test
    @WithMockUser(authorities = [&quot;SCOPE_read:client&quot;])
    fun `Authorized user, with required scope is OK`() {
        val entity = restTemplate.getForEntity&lt;String&gt;(&quot;/protected&quot;, String::class.java)
        assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    }
}

We would like to achieve something similar for our WebSocket endpoints... for example

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebSocketSecurityTest(@Autowired val restTemplate: TestRestTemplate) {

    @Test
    fun `WebSocket unauthorized user is unauthorized`() {
        val entity = restTemplate.getForEntity&lt;String&gt;(&quot;/protected-ws&quot;, String::class.java)
        assertThat(entity.statusCode).isEqualTo(HttpStatus.UNAUTHORIZED)
    }

    @Test
    @WithMockUser(authorities = [&quot;SCOPE_read:world&quot;])
    fun `WebSocket authorized user, without required scope is forbidden`() {
        val entity = restTemplate.getForEntity&lt;String&gt;(&quot;/protected-ws&quot;, String::class.java)
        assertThat(entity.statusCode).isEqualTo(HttpStatus.FORBIDDEN)
    }

    @Test
    @WithMockUser(authorities = [&quot;SCOPE_read:client-ws&quot;])
    fun `WebSocket Authorized user, with required scope is OK`() {
        val entity = restTemplate.getForEntity&lt;String&gt;(&quot;/protected-ws&quot;, String::class.java)
        // You can&#39;t open a WebSocket connection with a REST client. But this shows the request is authorized.
        assertThat(entity.statusCode).isEqualTo(HttpStatus.BAD_REQUEST)
    }
}

But this has several limitations:

  1. Our OK test has to check for a BAD_REQUEST response!
  2. We are unable to verify any actual payloads from the endpoint.

So it's only suitable for failure scenarios. Full source code for this example available on github.

答案1

得分: 0

我在STOMP测试的基础上找到了一种方法我意识到STOMP客户端是使用普通的WebSocket客户端我发现我能够以同样的方式构建测试而不需要STOMP包装

@Test
@WithMockUser(authorities = ["SCOPE_read:client-ws"])
fun `经过授权的WebSocket用户能够连接到端点`() {
    val latch = CountDownLatch(1)
    StandardWebSocketClient().execute(
        URI.create(String.format("ws://localhost:%d/protected-ws", port)),
        TestClientHandler(latch)
    ).subscribe()
    assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue()
}
英文:

I found an approach based on what I read on STOMP testing, I realised the STOMP client was using a plain WebSocket client and I found I was able to build tests the same way without the STOMP wrapper.

@Test
@WithMockUser(authorities = [&quot;SCOPE_read:client-ws&quot;])
fun `WebSocket authorized user is able to connect to endpoint`() {
    val latch = CountDownLatch(1)
    StandardWebSocketClient().execute(
        URI.create(String.format(&quot;ws://localhost:%d/protected-ws&quot;, port)),
        TestClientHandler(latch)
    ).subscribe()
    assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue()
}

huangapple
  • 本文由 发表于 2020年9月22日 17:20:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/64006722.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定