Websockets 和 Spring Boot Security 的错误 401

问题描述 投票:0回答:1

我正在使用 Spring Boot 3.1.0,并且我正在尝试在我的应用程序中实现一些 websockets。我正在按照互联网上的教程来实现它们并生成一些统一的测试以确保其正常工作。

代码与教程非常相似。一个配置类和一个控制器:

@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 上并不新鲜,我已经查看了其他问题的一些建议。解决方案与之前版本的 Spring Boot 略有不同,安全配置也随着版本的不同而不断发展。

如果启用 websockets 记录器:

logging.level.org.springframework.messaging=trace
logging.level.org.springframework.web.socket=trace

我可以看到身份验证标头在那里:

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]}

注: REST 端点的安全性运行良好。我有类似的测试,我生成一个具有某些角色的用户,然后使用它进行身份验证。 测试代码可在here获取,无需任何特殊配置即可执行。

我尝试过的:

  • 如您所见,在测试中,我尝试添加带有
    Bearer
    信息并生成 JWT 令牌的标头。没有成功。 JWT 生成方法必须正确,才能成功用于 REST 服务。
  • 我尝试将 Spring 安全配置中的套接字 URL 列入白名单。
  • 修复 CORS 问题。不过对于这个测试来说一定不是问题。
  • 在测试主类上使用
    @SpringBootApplication(exclude = SecurityAutoConfiguration.class)
    禁用安全性。同样的 401 错误。
  • 使用
    withSockJs()
    并将其删除。

我还尝试使用 jakarta websocket 包生成非 stomp websocket,问题完全相同:401 错误。

从我的角度来看,JWT 标头似乎未正确包含在测试中。但我看不到代码的问题。

java spring-boot spring-websocket
1个回答
0
投票

那么你有几个问题:

  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. 您应该使用我在评论中提到的
    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)));
© www.soinside.com 2019 - 2024. All rights reserved.