代码之家  ›  专栏  ›  技术社区  ›  Dachstein

Spring Webflux Websocket安全-基本身份验证

  •  8
  • Dachstein  · 技术社区  · 8 年前

    问题:我没有在Webflux项目中使用Websockets的Spring安全性。

    注意:我使用的是Kotlin而不是Java。

    依赖项:

    • 弹簧防尘套2.0.0

    • 弹簧安全5.0.3

    • Spring WebFlux 5.0.4

    重要更新: 我提出了一个春季问题bug(3月30日) here 一位Spring安全维护人员表示不支持它,但他们可以将其添加到 Spring Security 5.1.0 M2

    链接: Add WebFlux WebSocket Support #5188

    Webflux安全配置

    @EnableWebFluxSecurity
    class SecurityConfig
    {
        @Bean
        fun configure(http: ServerHttpSecurity): SecurityWebFilterChain
        {
    
            return http.authorizeExchange()
                .pathMatchers("/").permitAll()
                .anyExchange().authenticated()
                .and().httpBasic()
                .and().formLogin().disable().csrf().disable()
                .build()
        }
    
        @Bean
        fun userDetailsService(): MapReactiveUserDetailsService
        {
            val user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("pass")
                .roles("USER")
                .build()
    
            return MapReactiveUserDetailsService(user)
        }
    }
    

    Webflux Websocket配置

    @Configuration
    class ReactiveWebSocketConfiguration
    {
        @Bean
        fun webSocketMapping(handler: WebSocketHandler): HandlerMapping
        {
            val map = mapOf(Pair("/event", handler))
            val mapping = SimpleUrlHandlerMapping()
            mapping.order = -1
            mapping.urlMap = map
            return mapping
        }
    
        @Bean
        fun handlerAdapter() = WebSocketHandlerAdapter()
    
        @Bean
        fun websocketHandler() = WebSocketHandler { session ->
    
            // Should print authenticated principal BUT does show NULL
            println("${session.handshakeInfo.principal.block()}")
    
            // Just for testing we send hello world to the client
            session.send(Mono.just(session.textMessage("hello world")))
        }
    }
    

    客户端代码

    // Lets create a websocket and pass Basic Auth to it
    new WebSocket("ws://user:pass@localhost:8000/event");
    // ...
    

    观察

    1. 在websocket处理程序中,主体显示 无效的

    2. 客户端可以连接而无需验证。如果我这样做了 WebSocket("ws://localhost:8000/event") 没有基本的授权,它仍然可以工作!因此,Spring安全性不会对任何内容进行身份验证。

    我错过了什么? 我做错了什么?

    1 回复  |  直到 8 年前
        1
  •  3
  •   Serhii Povísenko Alferd Nobel    6 年前

    我可以建议你实施 own authentication mechanism 而不是利用Spring Security。

    什么时候 WebSocket 即将建立其使用的连接 handshake 带有 UPGRADE 要求基于此,我们的想法是使用我们自己的处理程序处理请求并在那里执行身份验证。

    幸运的是,Spring Boot RequestUpgradeStrategy 为此目的。最重要的是,基于您使用的应用服务器,Spring提供了这些策略的默认实现。正如我所用 Netty 班上的人都在吼叫 ReactorNettyRequestUpgradeStrategy

    以下是建议的原型:

    /**
     * Based on {@link ReactorNettyRequestUpgradeStrategy}
     */
    @Slf4j
    @Component
    public class BasicAuthRequestUpgradeStrategy implements RequestUpgradeStrategy {
    
        private int maxFramePayloadLength = NettyWebSocketSessionSupport.DEFAULT_FRAME_MAX_SIZE;
    
        private final AuthenticationService service;
    
        public BasicAuthRequestUpgradeStrategy(AuthenticationService service) {
            this.service = service;
        }
    
        @Override
        public Mono<Void> upgrade(ServerWebExchange exchange, //
                                  WebSocketHandler handler, //
                                  @Nullable String subProtocol, //
                                  Supplier<HandshakeInfo> handshakeInfoFactory) {
    
            ServerHttpResponse response = exchange.getResponse();
            HttpServerResponse reactorResponse = getNativeResponse(response);
            HandshakeInfo handshakeInfo = handshakeInfoFactory.get();
            NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory) response.bufferFactory();
    
            String originHeader = handshakeInfo.getHeaders()
                                               .getOrigin();// you will get ws://user:pass@localhost:8080
    
            return service.authenticate(originHeader)//returns Mono<Boolean>
                          .filter(Boolean::booleanValue)// filter the result
                          .doOnNext(a -> log.info("AUTHORIZED"))
                          .flatMap(a -> reactorResponse.sendWebsocket(subProtocol, this.maxFramePayloadLength, (in, out) -> {
    
                              ReactorNettyWebSocketSession session = //
                                      new ReactorNettyWebSocketSession(in, out, handshakeInfo, bufferFactory, this.maxFramePayloadLength);
    
                              return handler.handle(session);
                          }))
                          .switchIfEmpty(Mono.just("UNATHORIZED")
                                             .doOnNext(log::info)
                                             .then());
    
        }
    
        private static HttpServerResponse getNativeResponse(ServerHttpResponse response) {
            if (response instanceof AbstractServerHttpResponse) {
                return ((AbstractServerHttpResponse) response).getNativeResponse();
            } else if (response instanceof ServerHttpResponseDecorator) {
                return getNativeResponse(((ServerHttpResponseDecorator) response).getDelegate());
            } else {
                throw new IllegalArgumentException("Couldn't find native response in " + response.getClass()
                                                                                                 .getName());
            }
        }
    }
    

    此外,如果您在项目中对Spring安全性没有重要的逻辑依赖性,例如复杂的ACL逻辑,那么我建议您放弃它,甚至不要使用它。

    原因是,我认为Spring Security违反了反应式方法,因为它的MVC遗留思维方式。它使您的应用程序与大量的额外配置和“非表面”调谐纠缠在一起,并迫使工程师维护这些配置,使其变得越来越复杂。在大多数情况下,在根本不涉及Spring安全性的情况下,事情可以非常顺利地实现。只需创建一个组件并以适当的方式使用它。

    希望有帮助。