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

Dac*_*ein 6 spring-security websocket spring-webflux

问题:我没有将带有Websockets的Spring Security应用于Webflux项目。

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

依赖:

  • Spring Boot 2.0.0

  • Spring Security 5.0.3

  • 春季WebFlux 5.0.4

重要更新:我已经提出了一个春季号的bug(3月30日)在这里和Spring安全维护者的一个表示,其不支持,但他们可以将其添加为春季安全5.1.0 M2。

链接: 添加WebFlux WebSocket支持#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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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")))
    }
}
Run Code Online (Sandbox Code Playgroud)

客户代码

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

观测

  1. 在websocket处理程序中,主体显示null

  2. 客户端无需身份验证即可连接。如果我WebSocket("ws://localhost:8000/event")不使用基本身份验证,它仍然有效!因此,Spring Security不会对任何内容进行身份验证。

我缺少什么?我做错了什么?

pov*_*nko 4

我可以建议你实现你的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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

而且,如果你的项目中对Spring Security没有关键的逻辑依赖,比如复杂的ACL逻辑,那么我建议你摆脱它,甚至根本不使用它。

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

希望能帮助到你。