在 Spring Boot 测试中的纯二进制 websocket 连接期间保持 TestSecurityContextHolder

Posted

技术标签:

【中文标题】在 Spring Boot 测试中的纯二进制 websocket 连接期间保持 TestSecurityContextHolder【英文标题】:Perserving TestSecurityContextHolder during pure binary websocket connection in Spring Boot test 【发布时间】:2017-11-01 16:27:54 【问题描述】:

我有一个使用二进制 websocket(即 NO Stomp,AMQP 纯二进制缓冲区)的 spring boot (1.5.2.RELEASE) 应用程序。在我的测试中,我能够来回发送消息,效果很好。

但是,在 websocket 调用应用程序期间,我遇到了以下与 TestSecurityContexHolder 相关的无法解释的行为。

TestSecurityContextHolder 有一个正确设置的上下文,即我的客户 @WithMockCustomUser 正在设置它,我可以在测试开始时放置一个断点时验证它。即

public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser>, 

效果很好,我能够测试实现方法级别安全性的服务器端方法,例如

@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
public UserInterface get(String userName) 
…

我开始遇到的问题是,当我想对应用程序进行完整的集成测试时,即在测试中我只使用 java 特定的注释创建自己的 WebSocket 连接到应用程序,即(客户端中没有 spring 注释) .

ClientWebsocketEndpoint clientWebsocketEndpoint = new ClientWebsocketEndpoint(uri);

.

@ClientEndpoint
public class ClientWebsocketEndpoint 


    private javax.websocket.Session session = null;

    private ClientBinaryMessageHandler binaryMessageHandler;
    ByteArrayOutputStream buffer = new  ByteArrayOutputStream();

    public ClientWebsocketEndpoint(URI endpointURI) 
        try 
            WebSocketContainer container = ContainerProvider.getWebSocketContainer();
            container.connectToServer(this, endpointURI);


         catch (Exception e) 
            throw new RuntimeException(e);
        
    
    ….

如果尝试调用 websocket,那么我首先看到“SecurityContextPersistenceFilter”正在删除当前的 SecurityContex,这是完全可以预期的。我实际上希望它被删除,因为无论如何我都想测试身份验证,因为身份验证是 websocket 通信的一部分,而不是我的 http 调用的一部分,但困扰我的是以下内容。 到目前为止,我们只有一个 HTTP 调用(wireshark 证明了这一点)并且 SecurityContextPersistenceFilter 只清除了一次会话,并且通过在 clear 方法上设置断点,我看到它确实只被调用了一次。在客户端和服务器之间交换 6 个二进制消息(即在从客户端接收的 5 条消息中设置 SecurityContext)之后,我使用自定义令牌进行身份验证并将该令牌写入 TestSecurityContextHolder btw SecurityContexHolder,即

        SecurityContext realContext = SecurityContextHolder.getContext(); 
        SecurityContext testContext = TestSecurityContextHolder.getContext();

            token.setAuthenticated(true);

            realContext.setAuthentication(token);
            testContext.setAuthentication(token);

我看到该令牌的 hashCode 在购买的 ContexHolders 中是相同的,这意味着这是同一个对象。但是,下次我从客户端收到 ByteBuffer 时,SecuriyContextHolder.getAuthentication() 的结果为空。我首先认为他与 SecurityContextChannelInterceptor 有关,因为我阅读了一篇关于 websockets 和 spring 的好文章,即here,但情况似乎并非如此。至少在放置断点时,securityContextChannelInterceptor 不会在任何地方执行或调用,我看到 IDE 并没有停在那里。请注意,我故意不在这里扩展 AbstractWebSocketMessageBrokerConfigurer,因为我不需要它,即这是简单的二进制 websocket,没有(STOMP AMQP 等,即没有已知的消息传递)。但是我看到另一个类,即 WithSecurityContextTestExecutionListener 清除上下文

TestSecurityContextHolder.clearContext() line: 67   
WithSecurityContextTestExecutionListener.afterTestMethod(TestContext) line: 143 
TestContextManager.afterTestMethod(Object, Method, Throwable) line: 319 
RunAfterTestMethodCallbacks.evaluate() line: 94 

但只有在测试完成时!!!即,这是在 SecurityContext 为空之后的方式,尽管之前是使用客户令牌手动设置的。似乎过滤器之类的东西(但对于 websockets,即不是 HTTP)正在清除每个接收到的 WsFrame 上的 securityContext。我不知道那是什么。也可能是相对的:在服务器端,当我看到堆栈跟踪时,我可以观察到 StandardWebSocketHandlerAdapter 正在被调用,它正在创建 StandardWebSocketSession。

StandardWebSocketHandlerAdapter$4.onMessage(Object) line: 84    
WsFrameServer(WsFrameBase).sendMessageBinary(ByteBuffer, boolean) line: 592 

在 StandardWebSocketSession 中,我看到有一个“主要用户”字段。那么谁应该设置该主体,即我没有看到任何设置方法,唯一的设置方法是在“AbstractStandardUpgradeStrategy”期间,即在第一次调用中,但是一旦它建立了会话该怎么办?即 rfc6455 定义了

10.5。 WebSocket 客户端身份验证 该协议没有规定服务器可以使用的任何特定方式 在 WebSocket 握手期间对客户端进行身份验证。网络套接字 服务器可以使用任何可用的客户端身份验证机制

对我来说,这意味着我应该能够在后期随时定义用户主体。

这里是如何运行测试

@RunWith(SpringRunner.class)
@TestExecutionListeners(listeners= // ServletTestExecutionListener.class,
                                    DependencyInjectionTestExecutionListener.class,
                                    TransactionalTestExecutionListener.class,
                                    WithSecurityContextTestExecutionListener.class 
                                    
        )
@SpringBootTest(classes = 
                            SecurityWebApplicationInitializerDevelopment.class, 
                            SecurityConfigDevelopment.class, 
                            TomcatEmbededDevelopmentProfile.class, 
                            Internationalization.class, 
                            MVCConfigDevelopment.class,
                            PersistenceConfigDevelopment.class
 )

@WebAppConfiguration
@ActiveProfiles(SConfigurationProfiles.DEVELOPMENT_PROFILE)
@ComponentScan(
    "org.Server.*", 
    "org.Server.config.*", 
    "org.Server.config.persistence.*", 
    "org.Server.core.*",
    "org.Server.logic.**",

)


@WithMockCustomUser
public class workingWebSocketButNonWorkingAuthentication  
....

这是之前的部分

@Before
public void setup() 

    System.out.println("Starting Setup");

    mvc = MockMvcBuilders
             .webAppContextSetup(webApplicationContext)
             .apply(springSecurity())
             .build();

    mockHttpSession = new MockHttpSession(webApplicationContext.getServletContext(), UUID.randomUUID().toString());


为了总结我的问题是什么可能导致从客户端接收到另一个 ByteBuffer (WsFrame) 后从购买的 TestSecurityContextHolder 或 SecurityContextHolder 返回的安全上下文为空的行为?。

@5 月 31 日添加: 我在多次运行测试时偶然发现有时上下文不为空并且测试正常,即有时上下文确实充满了我提供的令牌。我想这与 Spring Security Authentication 绑定到 ThreadLocal 的事实有关,需要进一步挖掘。

@2017 年 6 月 6 日添加: 我可以确认知道问题出在线程中,即身份验证成功,但是在 http-nio-8081-exec-4 到 nio-8081-exec-5 之间跳转时,Security Contex 正在丢失,在这种情况下我已将 SecurityContextHolder 策略设置为 MODE_INHERITABLETHREADLOCAL。非常感谢任何建议。

2017 年 6 月 7 日添加 如果我添加 SecurityContextPropagationChannelInterceptor 在简单的 websocket 的情况下不会传播安全上下文。

@Bean
@GlobalChannelInterceptor(patterns = "*")
public ChannelInterceptor securityContextPropagationInterceptor()

    return new SecurityContextPropagationChannelInterceptor();

2017 年 6 月 12 日添加 使用 Async 表示法进行了测试,即在此处找到的表示法。 spring-security-async-principal-propagation 。这表明安全上下文在 spring 中在不同线程中执行的方法之间正确传输,但由于某种原因,同样的事情不适用于 Tomcat 线程,即 http-nio-8081-exec-4 , http-nio- 8081-exec-5 , http-nio-8081-exec-6 , http-nio-8081-exec-7 等。我感觉他与执行者有关,但到目前为止我不知道如何更改那个。

2017 年 6 月 13 日添加

通过打印当前线程和安全上下文,我发现第一个线程(即 http-nio-8081-exec-1)确实按预期填充了安全上下文,即每个模式 MODE_INHERITABLETHREADLOCAL,但是所有其他线程,即 http- nio-8081-exec-2,http-nio-8081-exec-3 没有。现在的问题是:这是预期的吗?我在这里找到了working with threads in Spring 声明

您不能在兄弟线程之间共享安全上下文(例如在线程池中)。此方法仅适用于由已包含填充 SecurityContext 的线程生成的子线程。

这基本上解释了它,但是由于在 Java 中没有办法找出线程的父级,我想问题是谁在创建线程 http-nio-8081-exec-2,是调度程序 servlet还是那个tomcat神奇地决定现在我将创建一个新线程。我之所以这么问是因为我看到有时部分代码是在同一个线程中执行的,或者根据运行情况而不同。

2017 年 6 月 14 日添加

由于我不想将所有内容合二为一,因此我创建了一个单独的问题,该问题处理寻找答案的问题,即在 Spring Boot 应用程序的情况下,如何将安全上下文传播到由 tomcat 创建的所有同级线程。找到here

【问题讨论】:

【参考方案1】:

我不是 100% 确定我理解了这个问题,但是 Java 调度程序 servlet 不太可能在没有被告知的情况下创建一个新线程。我认为tomcat在不同的线程中处理每个请求,所以这可能就是创建线程的原因。您可以查看this 和this 出去。祝你好运!

【讨论】:

感谢您的反馈。似乎tomcat正在创建这些线程。我目前正在寻找正确的方法来确保安全上下文在线程之间保持同步,即确保对象(安全上下文)在这些线程之间保持同步我不确定到底要使用什么 ReentrantLock CountDownLatch,信号量,班长,你知道春天有没有什么建议吗? 我不太确定具体情况,我认为 ReentrantLock 是你最好的选择。同步甚至可能会起作用。 因为 spring 有 DelegatingSecurityContextRunnable 但我不知道如何使用它来包装 tomcat 线程,目标是在兄弟线程之间同步 SecurityContex 所以我想我会使用 ReentrantLock 除非我找到用 DelegatingSecurityContextRunnable 回答如何做到这一点 你以前用过 DelegatingSecurityContextRunnable 做过类似的事情吗? 对不起,我没有。但这似乎真的很有趣!也许您可以就此提出一个单独的问题。

以上是关于在 Spring Boot 测试中的纯二进制 websocket 连接期间保持 TestSecurityContextHolder的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 2.0 WebFlux 教程 | 入门篇

Spring Boot 中的单元测试

Spring Boot 中的测试:JUnit

Spring-Boot 单元测试:ConstraintValidator 中的@Value

在 Spring Boot 中的 mockito 测试中的 if...else 语句问题

[翻译]Spring Boot 中的集成测试