我正在使用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头似乎没有正确地包含在测试中.但我看不出代码有什么问题.

推荐答案

你有几个问题:

  1. 你应该在考试中纠正你的ws path分.看一看常量
private String getWsPath() {
        return String.format("ws://127.0.0.1:%d/kendo-tournament-backend/%s", port,
                WebSocketConfiguration.SOCKETS_ROOT_URL);
}
  1. 正如我在 comments 中提到的,您应该使用SockJsClient.在其他情况下,您将收到400错误
this.webSocketStompClient = new WebSocketStompClient(new SockJsClient(Arrays.asList(new WebSocketTransport(new StandardWebSocketClient()))));
  1. 您应该使用StringMessageConverter,因为您在控制器中返回String:
this.webSocketStompClient.setMessageConverter(new StringMessageConverter());
  1. 您应该判断您的断言是否有效:
await().atMost(3, TimeUnit.SECONDS)
       .untilAsserted(() -> Assert.assertEquals(blockingQueue.poll(), String.format("Hello, %s!", TESTING_MESSAGE)));

Java相关问答推荐

Gmail Javi API批量处理太多请求

无法在Java中将hhmmss格式的时间解析为LocalTime

S的字符串表示是双重精确的吗?

如何在ApachePOI中将图像添加到工作表的页眉?

如何获取Instant#of EpochSecond(?)的最大值

如何让JVM在SIGSEGV崩溃后快速退出?

使SLF4J在Android中登录到Logcat,在测试中登录到控制台(Gradle依赖问题)

如何从日期中截取时间并将其传递给组件?

带有Health Check的Spring Boot BuildPack打破了本机映像构建过程

如何在不删除Java中已有内容的情况下覆盖文件内容?

Oj算法 MatrixR032从字符串、归一化和余弦相似度计算创建

Spring Boot中的应用程序.properties文件中未使用的属性

Android Studio模拟器没有互联网

对角线填充二维数组

接受类及其接口的Java类型(矛盾)

java.lang.ClassCastException:com.google.firebase.FirebaseException无法转换为com.google.fire base.auth.FirebaseAuthException

如何正确使用java.time类?

@此处不能应用可为null的批注

Java 8 中 ByteBuffer 和 BitSet 的奇怪行为

将在 Docker 中运行的 Spring Boot 连接到在 Docker 中运行的 PostgreSQL,无需 compose 文件?