【问题标题】:Spring Webflux Websocket Security - Basic AuthenticationSpring Webflux Websocket 安全 - 基本身份验证
【发布时间】:2018-09-07 18:37:43
【问题描述】:

问题:我没有让 Spring Security with Websockets 在 Webflux 项目中工作。

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

依赖关系:

  • Spring Boot 2.0.0

  • Spring Security 5.0.3

  • Spring WebFlux 5.0.4

重要更新:我提出了一个 Spring 问题错误(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 不会验证任何东西。

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

【问题讨论】:

  • 我也有同样的问题。有什么帮助吗??
  • 我们必须等到 Spring Security 团队实现了该功能(基于 Websockets 的基本身份验证)。但是,如果您等不及,则必须更改身份验证机制。
  • 加上问题或添加 cmets 让 Rob Winch 知道这个问题很重要 :)

标签: spring-security websocket spring-webflux


【解决方案1】:

我可以建议您实现 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 Security没有关键的逻辑依赖,比如复杂的ACL逻辑,那么我建议你去掉它,甚至不要使用它。

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

希望对你有帮助。

【讨论】:

  • 感谢您的详细解释和代码示例。
  • @Dachstein 很高兴它有用!
猜你喜欢
  • 2015-08-08
  • 2014-04-30
  • 1970-01-01
  • 2016-12-25
  • 2011-05-04
  • 1970-01-01
  • 2021-08-22
  • 2014-11-06
  • 2013-01-20
相关资源
最近更新 更多