使用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上下文传递安全上下文,替代线程绑定:
- 订阅方法中存入安全上下文到Reactor上下文:
@SubscriptionMapping("getNewPost") public Publisher<Post> getNewPost() { SecurityContext securityContext = SecurityContextHolder.getContext(); return this.publisher.asFlux() .contextWrite(ctx -> ctx.put(SecurityContext.class, securityContext)); }
- 修改嵌套字段的映射方法,从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:全局配置上下文传播
通过自定义拦截器,全局实现安全上下文的自动传递:
- 确保引入Spring Security Reactor依赖:
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-reactor</artifactId> </dependency>
- 添加全局拦截器配置:
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
相关产品推荐
相关产品推荐

