我有一个使用二进制 websocket 的 spring boot (1.5.2.RELEASE) 应用程序(即没有 Stomp、AMQP 纯二进制缓冲区)。在我的测试中,我能够来回发送消息,效果非常好。
但是,在对应用程序进行 websocket 调用期间,我遇到了以下与 TestSecurityContexHolder 相关的无法解释的行为。
TestSecurityContextHolder 有一个开始设置正确的上下文,即我的客户 @WithMockCustomUser 正在设置它,我可以在测试开始时放置断点时验证它。 IE。
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser>,
这非常有效,我能够测试实现方法级安全性的服务器端方法,例如
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
public UserInterface get(String userName) {
…
}
我开始遇到的问题是当我想要对应用程序进行完整的集成测试时,即在测试中我创建自己的与应用程序的 WebSocket 连接,仅使用 java 特定注释,即(客户端中没有 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 仅清除了一次会话,通过在清除方法上设置断点,我发现它确实只被调用了一次。在客户端和服务器之间交换 6 个二进制消息(即 SecurityContext 在从客户端收到的 5 个消息中设置)之后,我使用自定义令牌进行身份验证,并将该令牌写入 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() 的结果为 null。我首先认为他与 SecurityContextChannelInterceptor 有关,因为我读了一篇关于 websockets 和 spring 的好文章,即here https://robertleggett.wordpress.com/2015/05/27/websockets-with-spring-spring-security/但事实似乎并非如此。 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 为 null 之后的情况就是这样。似乎过滤器之类的东西(但对于 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 握手期间对客户端进行身份验证。 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 返回的 Security Context 为 null 的行为可能是什么原因导致的?
@5月31日添加:
我偶然发现,在多次运行测试时,有时上下文不为空,并且测试正常,即有时上下文确实填充了我提供的令牌。我想这与 Spring Security 身份验证绑定到 ThreadLocal 有关,需要进一步挖掘。
@2017 年 6 月 6 日添加:
我可以确认问题出在线程中,即身份验证成功,但是当在 http-nio-8081-exec-4 到 nio-8081-exec-5 之间跳转时,安全上下文正在丢失,在这种情况下我已将 SecurityContextHolder 策略设置为 MODE_INHERITABLETHREADLOCAL。非常感谢任何建议。
添加于 2017 年 6 月 7 日
如果我添加 SecurityContextPropagationChannelInterceptor 在简单的 websocket 情况下不会传播安全上下文。
@Bean
@GlobalChannelInterceptor(patterns = {"*"})
public ChannelInterceptor securityContextPropagationInterceptor()
{
return new SecurityContextPropagationChannelInterceptor();
}
添加于 2017 年 6 月 12 日
使用异步符号(即此处找到的符号)进行了测试。spring-security-async-principal-传播 http://www.baeldung.com/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 没有。现在的问题是:这是预期的吗?我在这里找到了在 Spring 中使用线程 http://www.ogrigas.eu/spring/2010/04/inherit-spring-security-context-in-child-threads的声明
您不能在同级线程之间共享安全上下文(例如在线程池中)。此方法仅适用于由已包含填充的 SecurityContext 的线程生成的子线程。
这基本上解释了它,但是由于在Java中没有办法找到线程的父级,我想问题是谁在创建线程http-nio-8081-exec-2,是调度程序servlet还是那个tomcat不知何故神奇地决定现在我将创建一个新线程。我这么问是因为我发现有时部分代码是在同一个线程中执行的,或者根据运行情况在不同的线程中执行。
添加于 2017 年 6 月 14 日
由于我不想将所有内容都放在一个中,因此我创建了一个单独的问题,该问题处理寻找答案的问题,即在 Spring Boot 应用程序的情况下如何将安全上下文传播到 tomcat 创建的所有同级线程。成立here https://stackoverflow.com/questions/44537723/how-to-run-delegatingsecuritycontextrunnable-every-time-when-tomcat-creates-new