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

使用Spring Security时GraphQL订阅丢失认证信息问题

问题:Spring Boot GraphQL订阅返回嵌套对象时丢失安全上下文

我用spring-boot-starter-graphql搭建GraphQL服务,通过Spring Security做基础认证保护。但当GraphQL订阅返回包含子类型的对象时,安全上下文会立即丢失认证信息,后续所有请求都会被重定向到登录页面。

精简配置代码

GraphQL Schema

type Post  {
    id: String!
    title: String! 
    author: Author!
}

type Author  {
    id: String!
    name: String!
}

type Subscription {
    getNewPost: Post
}

Java 核心代码

@SchemaMapping(typeName = "Post", field = "author")
public Author getAuthor(Post post) {
    return this.appService.getAuthorById(post.getAuthorId());
}

@Bean
Many<Post> publisher() {
    return Sinks.many().multicast().directBestEffort();
}

@SubscriptionMapping("getNewPost")
public Publisher<Post> getNewPost() {
    return this.publisher.asFlux();
}

public Post createPost(CreatePostInput postInput) {
    // ... 业务逻辑
    this.publisher.tryEmitNext(post);
    // ... 业务逻辑
}

关键现象

  • 订阅返回第一个对象前一切正常,返回后安全上下文直接变为空
  • 若订阅返回的对象(如Post)不含嵌套对象(如Author),则无此问题
  • 改用Java Kickstart实现时,不存在该问题

解决方法

这个问题本质是:GraphQL处理嵌套字段时会切换线程,而Spring Security的SecurityContext默认是线程绑定的,导致嵌套字段的@SchemaMapping方法无法继承订阅初始请求的安全上下文。下面是几种可行的解决方式:

方案1:手动在数据流中传播安全上下文

修改订阅方法,捕获初始请求的安全上下文,在每个元素处理时绑定到当前线程:

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

@SubscriptionMapping("getNewPost")
public Publisher<Post> getNewPost() {
    // 捕获当前请求的安全上下文
    SecurityContext currentContext = SecurityContextHolder.getContext();
    return this.publisher.asFlux()
            .map(post -> {
                // 绑定上下文到当前线程
                SecurityContextHolder.setContext(currentContext);
                try {
                    return post;
                } finally {
                    // 处理完成后清除,避免线程污染
                    SecurityContextHolder.clearContext();
                }
            });
}

方案2:利用Reactor上下文优雅传递

Spring GraphQL基于Reactor,推荐用Reactor上下文传递安全上下文,替代线程绑定:

  1. 订阅方法中存入安全上下文到Reactor上下文:
@SubscriptionMapping("getNewPost")
public Publisher<Post> getNewPost() {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    return this.publisher.asFlux()
            .contextWrite(ctx -> ctx.put(SecurityContext.class, securityContext));
}
  1. 修改嵌套字段的映射方法,从Reactor上下文取出并绑定:
import reactor.core.publisher.Mono;

@SchemaMapping(typeName = "Post", field = "author")
public Mono<Author> getAuthor(Post post) {
    return Mono.deferContextual(ctx -> {
        SecurityContext securityContext = ctx.get(SecurityContext.class);
        SecurityContextHolder.setContext(securityContext);
        try {
            return Mono.just(this.appService.getAuthorById(post.getAuthorId()));
        } finally {
            SecurityContextHolder.clearContext();
        }
    });
}

方案3:全局配置上下文传播

通过自定义拦截器,全局实现安全上下文的自动传递:

  1. 确保引入Spring Security Reactor依赖:
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-reactor</artifactId>
</dependency>
  1. 添加全局拦截器配置:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

@Configuration
public class GraphQlSecurityConfig {

    @Bean
    public WebGraphQlInterceptor securityContextPropagationInterceptor() {
        return (request, chain) -> {
            SecurityContext securityContext = SecurityContextHolder.getContext();
            return chain.next(request)
                    .contextWrite(ctx -> ctx.put(SecurityContext.class, securityContext));
        };
    }
}

配置后,所有数据获取方法都能通过Reactor上下文拿到安全上下文,无需手动处理线程绑定。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 10:05:21