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

时间:2018-03-28 22:40:15

标签: spring-security websocket spring-webflux

问题:我没有使用Websockets的Spring Security在Webflux项目中工作。

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

依赖关系:

  • Spring Boot 2.0.0

  • Spring Security 5.0.3

  • Spring WebFlux 5.0.4

重要更新:我提出了一个Spring Issue错误(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处理程序中,主体显示 null

  2. 客户端可以在未经过身份验证的情况下进行连接。如果我没有基本身份验证WebSocket("ws://localhost:8000/event")它仍然有效!因此Spring Security不会对任何内容进行身份验证。

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

1 个答案:

答案 0 :(得分:0)

我建议您实施own authenticator

基本思想如下:当WebSocket connection即将建立时,它使用handshake mechanism并附带UPGRADE request。因此,在这种情况下,我们将对请求使用自己的处理程序并在其中执行身份验证。幸运的是,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 Security的关键逻辑依赖性(例如复杂的ACL逻辑或类似的逻辑),那么我建议您摆脱它并且不要使用它。原因是我认为它是一种反应式方法违规者,因为我会说它是MVC遗留的心态(其后的Servlets世界起源于同步范例)。

希望有帮助。