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

Spring Webflux Websocket Security - Basic Authentication

问题:我没有得到 Spring Websockets 的安全性以在 Webflux 项目中工作。

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

依赖项:

重要更新: 我提出了一个 Spring 问题错误(3 月 30 日)here and one of the Spring security maintainers said its NOT SUPPORTED but they can add it for Spring Security 5.1.0 M2

LINK: 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 处理程序中,主体显示 null

  2. 客户端无需验证即可连接。如果我在没有 Basic Auth 的情况下执行 WebSocket("ws://localhost:8000/event") 它仍然有效!因此 Spring 安全性不验证任何内容。

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

我建议您实施 own authentication mechanism 而不是利用 Spring 安全性。

WebSocket 连接即将建立时,它使用 handshake 机制伴随 UPGRADE 请求。基于此,我们的想法是使用我们自己的处理程序来处理请求并在那里执行身份验证。

幸运的是,Spring 引导有 RequestUpgradeStrategy for such purpose. On top of that, based on the application server what you use, Spring provides a default implementation of those strategies. As I use Netty bellow the class would be 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 安全是反应式方法的违反者,因为它,我想说,是 MVC 遗留的思维方式。它使您的应用程序与大量额外配置和“非表面”调整纠缠在一起,并迫使工程师维护这些配置,使它们变得越来越复杂。在大多数情况下,事情可以非常顺利地实施,而根本不会触及 Spring 安全性。只需创建一个组件并以适当的方式使用它。

希望对您有所帮助。