如何用DGS与Spring Security实现GraphQL订阅授权
问题原因分析
- SecurityContext 无法跨线程传递:你使用了
NullSecurityContextRepository,它不会持久化SecurityContext,在WebSocket握手的HTTP请求结束后,SecurityContextPersistenceFilter会直接清空SecurityContextHolder。而GraphQL订阅基于WebSocket,握手完成后,后续的事件推送是在新线程执行的,默认的ThreadLocal无法将初始认证信息传递到这些新线程,导致方法安全校验时SecurityContext为空。 - Query/Mutation与订阅的差异:Query和Mutation是单次HTTP请求,整个处理流程在同一个线程内完成,认证信息能保留到方法校验阶段;但订阅是长连接,后续事件处理线程无法继承初始握手时的SecurityContext。
解决方案
方案1:用DelegatingSecurityContextPublisher包装订阅Publisher
直接在数据获取器中,将返回的Publisher用DelegatingSecurityContextPublisher包装,确保每个订阅事件都在初始的SecurityContext环境下执行:
@DgsComponent @RequiredArgsConstructor public class StocksDataFetcher implements SupportsGlobalObjectIdentification { private final StocksService service; @DgsSubscription @Secured("ROLE_myRole") public Publisher<Stock> stocks(DataFetchingEnvironment dfe) { SecurityContext currentContext = SecurityContextHolder.getContext(); // 包装Publisher,传递当前SecurityContext return new DelegatingSecurityContextPublisher<>(service.getStocksPublisher(), currentContext); } }
方案2:切换到可继承的SecurityContextHolder策略
配置Spring Security使用InheritableThreadLocalSecurityContextHolderStrategy,让子线程自动继承父线程的SecurityContext:
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfig { // 配置可继承的SecurityContextHolder策略 @Bean public SecurityContextHolderStrategy securityContextHolderStrategy() { return new InheritableThreadLocalSecurityContextHolderStrategy(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { final var resolver = new DefaultBearerTokenResolver(); resolver.setAllowUriQueryParameter(true); http.cors() .and() .authorizeHttpRequests().anyRequest().authenticated() .and() .oauth2ResourceServer() .bearerTokenResolver(resolver) .jwt(); return http.build(); } }
注意:移除原配置中的NullSecurityContextRepository,它会阻止SecurityContext的传递。
方案3:通过WebSocket握手拦截器保存认证信息
通过握手拦截器将SecurityContext存入WebSocket会话,在后续消息处理时恢复:
- 创建握手拦截器:
@Component public class SecurityWebSocketHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { SecurityContext context = SecurityContextHolder.getContext(); if (context.getAuthentication() != null) { attributes.put("SPRING_SECURITY_CONTEXT", context); } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {} }
- 配置WebSocket消息处理时恢复SecurityContext:
@Configuration public class DgsWebSocketConfig { private final SecurityWebSocketHandshakeInterceptor handshakeInterceptor; public DgsWebSocketConfig(SecurityWebSocketHandshakeInterceptor handshakeInterceptor) { this.handshakeInterceptor = handshakeInterceptor; } @Bean public WebSocketHandlerDecoratorFactory webSocketHandlerDecoratorFactory() { return handler -> new WebSocketHandlerDecorator(handler) { @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { SecurityContext context = (SecurityContext) session.getAttributes().get("SPRING_SECURITY_CONTEXT"); if (context != null) { SecurityContextHolder.setContext(context); } try { super.handleMessage(session, message); } finally { SecurityContextHolder.clearContext(); } } }; } }
内容的提问来源于stack exchange,提问作者André
相关产品推荐
相关产品推荐

