我正在使用Spring Boot 3.1.0,并try 在我的应用程序中实现一些WebSocket.我正在遵循互联网上的一个教程来实现它们,并生成一些单一的测试,以确保它们是有效的.
代码与教程中的代码非常相似.配置类和控制器:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
//Where is listening to messages
public static final String SOCKET_RECEIVE_PREFIX = "/app";
//Where messages will be sent.
public static final String SOCKET_SEND_PREFIX = "/topic";
//URL where the client must subscribe.
public static final String SOCKETS_ROOT_URL = "/ws-endpoint";
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(SOCKETS_ROOT_URL)
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes(SOCKET_RECEIVE_PREFIX)
.enableSimpleBroker(SOCKET_SEND_PREFIX);
}
}
@Controller
public class WebSocketController {
@MessageMapping("/welcome")
@SendTo("/topic/greetings")
public String greeting(String payload) {
System.out.println("Generating new greeting message for " + payload);
return "Hello, " + payload + "!";
}
@SubscribeMapping("/chat")
public MessageContent sendWelcomeMessageOnSubscription() {
return new MessageContent(String.class.getSimpleName(), "Testing");
}
}
本教程中没有介绍的额外信息是安全配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
private static final String[] AUTH_WHITELIST = {
// -- Swagger
"/v3/api-docs/**", "/swagger-ui/**",
// Own
"/",
"/info/**",
"/auth/public/**",
//Websockets
WebSocketConfiguration.SOCKETS_ROOT_URL,
WebSocketConfiguration.SOCKET_RECEIVE_PREFIX,
WebSocketConfiguration.SOCKET_SEND_PREFIX,
WebSocketConfiguration.SOCKETS_ROOT_URL + "/**",
WebSocketConfiguration.SOCKET_RECEIVE_PREFIX + "/**",
WebSocketConfiguration.SOCKET_SEND_PREFIX + "/**"
};
private final JwtTokenFilter jwtTokenFilter;
@Value("${server.cors.domains:null}")
private List<String> serverCorsDomains;
@Autowired
public WebSecurityConfig(JwtTokenFilter jwtTokenFilter) {
this.jwtTokenFilter = jwtTokenFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
//Will use the bean KendoUserDetailsService.
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//Disable cors headers
.cors(cors -> cors.configurationSource(generateCorsConfigurationSource()))
//Disable csrf protection
.csrf(AbstractHttpConfigurer::disable)
//Sessions should be stateless
.sessionManagement(httpSecuritySessionManagementConfigurer ->
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
httpSecurityExceptionHandlingConfigurer
.authenticationEntryPoint((request, response, ex) -> {
RestServerLogger.severe(this.getClass().getName(), ex.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
})
.accessDeniedHandler((request, response, ex) -> {
RestServerLogger.severe(this.getClass().getName(), ex.getMessage());
response.sendError(HttpServletResponse.SC_FORBIDDEN, ex.getMessage());
})
)
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated());
return http.build();
}
private CorsConfigurationSource generateCorsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
if (serverCorsDomains == null || serverCorsDomains.contains("*")) {
configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
} else {
configuration.setAllowedOrigins(serverCorsDomains);
configuration.setAllowCredentials(true);
}
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.addExposedHeader(HttpHeaders.AUTHORIZATION);
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
测试是:
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Test(groups = "websockets")
@AutoConfigureMockMvc(addFilters = false)
public class BasicWebsocketsTests extends AbstractTestNGSpringContextTests {
@BeforeClass
public void authentication() {
//Generates a user for authentication on the test.
AuthenticatedUser authenticatedUser = authenticatedUserController.createUser(null, USER_NAME, USER_FIRST_NAME, USER_LAST_NAME, USER_PASSWORD, USER_ROLES);
headers = new WebSocketHttpHeaders();
headers.set("Authorization", "Bearer " + jwtTokenUtil.generateAccessToken(authenticatedUser, "127.0.0.1"));
}
@BeforeMethod
public void setup() throws ExecutionException, InterruptedException, TimeoutException {
WebSocketClient webSocketClient = new StandardWebSocketClient();
this.webSocketStompClient = new WebSocketStompClient(webSocketClient);
this.webSocketStompClient.setMessageConverter(new MappingJackson2MessageConverter());
}
@Test
public void echoTest() throws ExecutionException, InterruptedException, TimeoutException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
StompSession session = webSocketStompClient.connectAsync(getWsPath(), this.headers,
new StompSessionHandlerAdapter() {
}).get(1, TimeUnit.SECONDS);
session.subscribe("/topic/greetings", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
blockingQueue.add((String) payload);
}
});
session.send("/app/welcome", TESTING_MESSAGE);
await().atMost(1, TimeUnit.SECONDS)
.untilAsserted(() -> Assert.assertEquals("Hello, Mike!", blockingQueue.poll()));
}
得到的结果是:
ERROR 2024-01-05 20:58:55.068 GMT+0100 c.s.k.l.RestServerLogger [http-nio-auto-1-exec-1] - com.softwaremagico.kt.rest.security.WebSecurityConfig$$SpringCGLIB$$0: Full authentication is required to access this resource
java.util.concurrent.ExecutionException: jakarta.websocket.DeploymentException: Failed to handle HTTP response code [401]. Missing [WWW-Authenticate] header in response.
这在StackOverflow上并不 fresh ,我已经回顾了其他问题的一些建议.解决方案与之前版本的Spring Boot略有不同,安全配置也随着版本的变化而变化.
如果启用WebSockets记录器:
logging.level.org.springframework.messaging=trace
logging.level.org.springframework.web.socket=trace
我可以看到auth标头在那里:
TRACE 2024-01-06 08:25:54.109 GMT+0100 o.s.w.s.c.s.StandardWebSocketClient [SimpleAsyncTaskExecutor-1] - Handshake request headers: {Authorization=[Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxLFRlc3QuVXNlciwxMjcuMC4wLjEsIiwiaXNzIjoiY29tLnNvZnR3YXJlbWFnaWNvIiwiaWF0IjoxNzA0NTI1OTUzLCJleHAiOjE3MDUxMzA3NTN9.-ploHleAF6IpUmP4IPzLV1nYNHnigpamYgS9e3Gp183SLri-37QZA2TDKIbE6iTDCunF0JRYry7xSsq_Op1UgQ], Sec-WebSocket-Key=[cxyjc2DjRRfm/elvG0261A==], Connection=[upgrade], Sec-WebSocket-Version=[13], Host=[127.0.0.1:38373], Upgrade=[websocket]}
Note: 对于REST端点,安全性运行良好.我也有类似的测试,我生成一个具有某些角色的用户,然后使用它进行身份验证. 测试代码here可用,无需任何特殊配置即可执行.
What I have tried:个
- 正如您所看到的,在测试中,我try 使用生成的JWT令牌添加带有
Bearer
信息的头.没有成功.JWT生成方法必须正确,因为它成功地在REST服务上使用. - 我已经try 在Spring安全配置上将套接字URL列入白名单.
- 来解决CORS问题.但对于这项测试来说,一定不是问题.
- 在测试主类上使用
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
禁用安全性.相同的401错误. - 使用
withSockJs()
并将其删除.
我还try 用Jakarta WebSocket包生成一个非STOMP WebSocket,问题完全一样:401错误.
在我看来,JWT头似乎没有正确地包含在测试中.但我看不出代码有什么问题.