You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring WebFlux设置Security Context后仍为空问题求助

问题描述

基于Spring Boot 3.1.8和Spring Security 6.1.6开发响应式应用,在Kafka消费者中处理认证逻辑时遇到问题:使用ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))设置安全上下文后,同一响应链中立即访问该上下文却发现为空。相关代码片段如下:

public <T> Mono<Void> authenticateAndVerifyContext(ReceiverRecord<String, T> record) {

        return extractCredentialsFromBasicAuth(record)
                .flatMap(this::authenticate)
                .flatMap(authentication -> {
                    SecurityContext securityContext = new SecurityContextImpl(authentication);
                    log.info("securityContext: {}", securityContext);
                    return Mono.deferContextual(view -> Mono.empty())
                               .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
                               .then(ReactiveSecurityContextHolder.getContext()
                                                                  .flatMap(context -> {
                                                                      if (context.getAuthentication() != null && context.getAuthentication().isAuthenticated()) {
                                                                          log.info("Security context successfully updated. User: {}", context.getAuthentication().getName());
                                                                      } else {
                                                                          log.error("Authentication failed");
                                                                      }
                                                                      return Mono.empty();
                                                                  })
                                                                  .switchIfEmpty(Mono.defer(() -> {
                                                                      log.error("Security context is empty");
                                                                      return Mono.empty();
                                                                  })));
                }).then();
    }

    private <T> Mono<UsernamePasswordAuthenticationToken> extractCredentialsFromBasicAuth(
            final ReceiverRecord<String, T> record) {

        log.info("kafka authorization: {}",
                 new String(record.headers().lastHeader(AUTHORIZATION_KEY).value(), StandardCharsets.UTF_8));

        return Mono.justOrEmpty(record.headers().lastHeader(AUTHORIZATION_KEY))
                   .map(header -> new String(header.value(), StandardCharsets.UTF_8))
                   .map(this::decodeBasicAuthHeader)
                   .flatMap(this::createAuthenticationToken);
    }

    private String decodeBasicAuthHeader(final String headerValue) {

        return new String(Base64.getDecoder().decode(headerValue.substring(6)), StandardCharsets.UTF_8);
    }


    private Mono<UsernamePasswordAuthenticationToken> createAuthenticationToken(final String credentials) {

        final String[] parts = credentials.split(":", 2);
        if (parts.length == 2) {
            return Mono.just(new UsernamePasswordAuthenticationToken(parts[0], parts[1]));
        } else {
            return Mono.error(new IllegalArgumentException("Invalid basic authentication token"));
        }
    }

问题原因分析

核心问题在于Reactor上下文的作用范围限制:

  • .contextWrite()操作符仅对其所在流链中当前及后续的操作生效,它是通过流的订阅链传递上下文的。
  • 当前代码中,.contextWrite()之后调用.then(),而.then()会启动一个全新的独立流(即ReactiveSecurityContextHolder.getContext()),这个新流不会继承前面设置的上下文,因此获取到的上下文为空。

解决思路

需要将访问安全上下文的操作纳入到.contextWrite()的作用域内,确保上下文能在同一个流链中传递。具体修改方式有两种:

方式一:调整流链结构,让上下文传递到后续操作

去掉多余的Mono.deferContextual,将获取上下文的操作直接链式接在.contextWrite()之后,确保上下文能覆盖后续操作:

return extractCredentialsFromBasicAuth(record)
        .flatMap(this::authenticate)
        .flatMap(authentication -> {
            SecurityContext securityContext = new SecurityContextImpl(authentication);
            log.info("securityContext: {}", securityContext);
            // 直接在contextWrite的流链中串联获取上下文的操作
            return Mono.empty()
                       .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
                       .then(ReactiveSecurityContextHolder.getContext())
                       .flatMap(context -> {
                           if (context.getAuthentication() != null && context.getAuthentication().isAuthenticated()) {
                               log.info("Security context successfully updated. User: {}", context.getAuthentication().getName());
                           } else {
                               log.error("Authentication failed");
                           }
                           return Mono.empty();
                       })
                       .switchIfEmpty(Mono.defer(() -> {
                           log.error("Security context is empty");
                           return Mono.empty();
                       }));
        }).then();

方式二:使用withSecurityContext直接包裹上下文操作

这种方式更简洁,withSecurityContext会直接创建带有指定上下文的流容器,后续操作自然能继承该上下文:

return extractCredentialsFromBasicAuth(record)
        .flatMap(this::authenticate)
        .flatMap(authentication -> {
            SecurityContext securityContext = new SecurityContextImpl(authentication);
            log.info("securityContext: {}", securityContext);
            // 用withSecurityContext直接包裹需要访问上下文的逻辑
            return ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))
                    .then(ReactiveSecurityContextHolder.getContext())
                    .flatMap(context -> {
                        if (context.getAuthentication() != null && context.getAuthentication().isAuthenticated()) {
                            log.info("Security context successfully updated. User: {}", context.getAuthentication().getName());
                        } else {
                            log.error("Authentication failed");
                        }
                        return Mono.empty();
                    })
                    .switchIfEmpty(Mono.defer(() -> {
                        log.error("Security context is empty");
                        return Mono.empty();
                    }));
        }).then();

另外需要注意:在Kafka响应式消费者场景中,每个消息的处理都应该是独立的上下文,上述修改也能确保不同消息的认证上下文不会互相干扰。

内容的提问来源于stack exchange,提问作者Alejo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 07:05:23