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

Spring Boot Webflux Security:服务类读取Principal及测试问题

Webflux中Principal获取与测试实践

针对你在Spring Boot 2 + Webflux函数式编程中遇到的两个问题,我结合Spring Security的反应式特性来给你详细解答:

问题1:函数式代码中用SecurityContextHolder获取Principal是否为最佳实践?

首先要明确:在Webflux的反应式编程模型中,直接使用SecurityContextHolder.getContext()不是最佳实践,甚至会引发问题。原因在于:

  • Webflux基于Reactor框架,采用异步非阻塞的线程模型,线程会被复用,传统的ThreadLocal(SecurityContextHolder默认依赖它)无法正确绑定到Reactor的订阅链上,可能导致上下文泄露、获取到错误的用户信息,或者在某些线程切换场景下获取到null。

那更合适的方案是什么?
推荐使用Reactor Context来传递Security上下文,它与Reactor的订阅生命周期绑定,而非线程绑定,完美适配反应式场景。具体实现方式:

  1. 在handler函数中,先获取当前的Authentication(从ServerRequest的Principal),然后通过contextWrite将SecurityContext写入Reactor Context:
public Mono<ServerResponse> all(ServerRequest serverRequest) {
    return serverRequest.principal()
        .cast(JwtAuthenticationToken.class)
        .flatMap(auth -> {
            SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
            securityContext.setAuthentication(auth);
            return ReactiveResponses.listResponse(
                this.projectService.all()
                    .contextWrite(ctx -> ctx.put(SecurityContext.class, securityContext))
            );
        });
}
  1. 在服务类中,通过Mono.deferContextual读取Reactor Context中的SecurityContext:
public Flux<Project> all() {
    return Mono.deferContextual(ctx -> {
        SecurityContext securityContext = ctx.get(SecurityContext.class);
        JwtAuthenticationToken auth = (JwtAuthenticationToken) securityContext.getAuthentication();
        String userId = auth.getName(); // 或从JWT claims中获取更详细信息
        return projectRepository.findByOwnerUserId(userId);
    });
}

对比逐层传递Principal的方式:

  • 逐层传递虽然显式,但会让方法参数变得繁琐,尤其是多层服务调用时;
  • 用Reactor Context传递的方式,既避免了参数传递的冗余,又符合反应式编程的最佳实践,同时保证了上下文的正确性。

问题2:如何测试依赖Security上下文的服务实现?

针对你测试时获取null的问题,核心原因是之前用SecurityContextHolder的ThreadLocal方式不适合反应式测试,改用Reactor Context后,测试可以这样调整:

服务层测试修改示例

@DataMongoTest
@Import({ProjectServiceImpl.class})
class ProjectServiceImplTest extends BaseServiceTest {
    @Autowired
    ProjectServiceImpl projectService;
    @Autowired
    ProjectRepository projectRepository;

    @BeforeEach
    void setUp() {
        // 初始化测试数据
    }

    @Test
    public void all_returnsOnlyOwnedProjects() {
        // 1. 构造模拟的JwtAuthenticationToken
        Jwt jwt = Jwt.withTokenValue("mock-jwt")
                .claim("sub", "uuid2")
                .build();
        JwtAuthenticationToken authToken = new JwtAuthenticationToken(jwt, Collections.emptyList());
        
        // 2. 构造SecurityContext并写入Reactor Context
        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authToken);

        // 3. 准备测试数据
        Flux<Project> saved = projectRepository.saveAll(
            Flux.just(
                new Project(null, "First", "uuid"),
                new Project(null, "Second", "uuid2"),
                new Project(null, "Third", "uuid3")
            )
        );

        // 4. 调用服务方法并写入上下文
        Flux<Project> all = projectService.all()
                .contextWrite(ctx -> ctx.put(SecurityContext.class, securityContext));

        // 5. 验证结果
        StepVerifier
            .create(saved.thenMany(all))
            .consumeNextWith(project -> {
                assertThat(project.getOwnerUserId()).isEqualTo("uuid2");
            })
            .verifyComplete();
    }
}

端点测试的调整(解决@WithMockUser类型不一致问题)

在Webflux的端点测试中,@WithMockUser是基于ThreadLocal的,不适合反应式场景,应该使用Spring Security提供的SecurityMockServerConfigurers来配置测试服务器的安全上下文:

@WebFluxTest(ProjectHandler.class)
class ProjectHandlerTest {
    @Autowired
    WebTestClient webTestClient;

    @Test
    void all_returnsUserProjects() {
        // 模拟JWT认证的用户
        Jwt jwt = Jwt.withTokenValue("mock-jwt")
                .claim("sub", "uuid2")
                .build();
        JwtAuthenticationToken authToken = new JwtAuthenticationToken(jwt, Collections.emptyList());

        webTestClient.mutateWith(SecurityMockServerConfigurers.mockUser().authentication(authToken))
                .get()
                .uri("/projects")
                .exchange()
                .expectStatus().isOk()
                .expectBodyList(Project.class)
                .consumeWith(result -> {
                    assertThat(result.getResponseBody()).allMatch(p -> p.getOwnerUserId().equals("uuid2"));
                });
    }
}

这样就能保证测试时上下文的正确性,同时匹配你实际使用的JwtAuthenticationToken类型。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:22:15