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

使用SSE传输时从受保护MCP工具获取Spring Security Principal

问题分析

按照官方文档配置MCP Server OAuth2认证后,MCP Inspector可正常携带Bearer令牌连接,但工具代码中SecurityContextHolder.getContext().getAuthentication()始终返回null。核心原因是:MCPSyncServer会阻塞MCPAsyncServer提供的Mono,导致工具代码运行在弹性调度器线程,而非填充了SecurityContext的IO线程,上下文未被传递。尝试配置DelegatingSecurityContextAsyncTaskExecutor无效,因为Reactor Schedulers并未使用该执行器。

当前通过反射捕获SessionID与Authentication并关联存储的临时方案,依赖MCP内部类结构,版本升级时易失效,维护成本高。以下是三种更优的解决方案:


方案1:基于Reactor Context传递SecurityContext

利用Reactor的Context机制,在会话创建时将Authentication存入上下文,工具执行时从Context中提取,完全符合响应式编程模型。

实现步骤

  1. 自定义会话工厂包装类,创建会话时将当前Authentication存入Reactor Context
  2. 工具执行时通过Mono.deferContextual获取上下文信息

代码示例

@Component
public class ContextAwareMcpSessionFactory implements McpServerSession.Factory {
    private final McpServerSession.Factory delegate;

    public ContextAwareMcpSessionFactory(McpServerSession.Factory delegate) {
        this.delegate = delegate;
    }

    @Override
    public McpServerSession create(McpServerTransport transport) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        McpServerSession originalSession = delegate.create(transport);
        
        // 包装会话,处理请求时注入Reactor Context
        return new McpServerSession() {
            @Override
            public String getId() {
                return originalSession.getId();
            }

            @Override
            public Mono<Void> handle(McpSchema.JSONRPCMessage message) {
                return originalSession.handle(message)
                        .contextWrite(ctx -> ctx.put("AUTHENTICATION", auth));
            }

            // 其他方法全部委托给originalSession
            @Override
            public void init(McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo) {
                originalSession.init(clientCapabilities, clientInfo);
            }

            @Override
            public <T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef) {
                return originalSession.sendRequest(method, requestParams, typeRef);
            }

            @Override
            public Mono<Void> sendNotification(String method, Object params) {
                return originalSession.sendNotification(method, params);
            }

            @Override
            public Mono<Void> closeGracefully() {
                return originalSession.closeGracefully();
            }

            @Override
            public void close() {
                originalSession.close();
            }

            @Override
            public Mono<Void> sendNotification(String method) {
                return originalSession.sendNotification(method);
            }
        };
    }
}

工具回调中获取Authentication:

@Bean
public ToolCallback userSecrets() {
    return FunctionToolCallback.builder("userSecrets", (UserSecretsInput input, ToolContext context) ->
            Mono.deferContextual(ctx -> {
                Authentication auth = ctx.getOrDefault("AUTHENTICATION", null);
                if (auth == null) {
                    throw new RuntimeException("Authentication not found");
                }
                return Mono.just("user specific data for " + auth.getName() + " with input: " + input);
            }).block() // 同步工具需阻塞获取结果
    )
    .inputType(UserSecretsInput.class)
    .description("Provide user specific data")
    .build();
}

record UserSecretsInput(String input){};

方案2:自定义ToolExecutor传递SecurityContext

扩展MCP的工具执行器,在调用工具回调前将Authentication设置到当前线程的SecurityContext,执行完成后清理上下文,适合同步工具场景。

实现步骤

  1. 实现ToolExecutor接口,在执行逻辑中注入Authentication
  2. 注册自定义ToolExecutor Bean替换默认实现

代码示例

@Component
public class SecurityContextPropagatingToolExecutor implements ToolExecutor {
    private final ToolExecutor delegate;
    private final ConcurrentMap<String, Authentication> sessionAuthMap = new ConcurrentHashMap<>();

    public SecurityContextPropagatingToolExecutor(ToolExecutor delegate) {
        this.delegate = delegate;
    }

    // 会话创建时调用此方法存储Authentication
    public void registerSessionAuth(String sessionId, Authentication auth) {
        sessionAuthMap.put(sessionId, auth);
    }

    // 会话关闭时清理
    public void removeSessionAuth(String sessionId) {
        sessionAuthMap.remove(sessionId);
    }

    @Override
    public Object execute(ToolInvocation invocation, ToolContext context) {
        // 从ToolContext获取SessionID
        String sessionId = McpToolUtils.getMcpExchange(context)
                .map(exchange -> exchange.getSession().getId())
                .orElseThrow(() -> new RuntimeException("Session ID not found"));

        Authentication auth = sessionAuthMap.get(sessionId);
        if (auth == null) {
            throw new RuntimeException("Authentication not found for session");
        }

        // 设置SecurityContext
        SecurityContext originalContext = SecurityContextHolder.getContext();
        try {
            SecurityContextHolder.getContext().setAuthentication(auth);
            return delegate.execute(invocation, context);
        } finally {
            // 恢复原上下文
            SecurityContextHolder.setContext(originalContext);
        }
    }
}

配合会话工厂注册Authentication:

@Component
public class AuthTrackingMcpSessionFactory implements McpServerSession.Factory {
    private final McpServerSession.Factory delegate;
    private final SecurityContextPropagatingToolExecutor toolExecutor;

    public AuthTrackingMcpSessionFactory(McpServerSession.Factory delegate, SecurityContextPropagatingToolExecutor toolExecutor) {
        this.delegate = delegate;
        this.toolExecutor = toolExecutor;
    }

    @Override
    public McpServerSession create(McpServerTransport transport) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        McpServerSession session = delegate.create(transport);
        
        if (auth != null) {
            toolExecutor.registerSessionAuth(session.getId(), auth);
        }

        // 包装会话,关闭时清理
        return new McpServerSessionWrapper(session, session.getId());
    }

    private class McpServerSessionWrapper extends McpServerSession {
        private final McpServerSession delegate;
        private final String sessionId;

        public McpServerSessionWrapper(McpServerSession delegate, String sessionId) {
            super("", null, null, null, null, null);
            this.delegate = delegate;
            this.sessionId = sessionId;
        }

        @Override
        public void close() {
            toolExecutor.removeSessionAuth(sessionId);
            delegate.close();
        }

        @Override
        public Mono<Void> closeGracefully() {
            toolExecutor.removeSessionAuth(sessionId);
            return delegate.closeGracefully();
        }

        // 其他方法委托给delegate...
        @Override
        public String getId() {
            return delegate.getId();
        }

        @Override
        public void init(McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo) {
            delegate.init(clientCapabilities, clientInfo);
        }

        @Override
        public <T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef) {
            return delegate.sendRequest(method, requestParams, typeRef);
        }

        @Override
        public Mono<Void> sendNotification(String method, Object params) {
            return delegate.sendNotification(method, params);
        }

        @Override
        public Mono<Void> handle(McpSchema.JSONRPCMessage message) {
            return delegate.handle(message);
        }

        @Override
        public Mono<Void> sendNotification(String method) {
            return delegate.sendNotification(method);
        }
    }
}

方案3:基于会话属性存储Authentication(无硬编码反射)

通过自定义WebMvcSseServerTransportProvider,在创建会话时直接将Authentication与SessionID关联存储,避免依赖内部类反射。

实现步骤

  1. 自定义WebMvcSseServerTransportProvider,覆盖会话创建逻辑存储Authentication
  2. 工具回调中通过公开API获取SessionID并关联Authentication

代码示例

@Component
public class AuthStoringWebMvcSseServerTransportProvider extends WebMvcSseServerTransportProvider {
    private final ConcurrentMap<String, Authentication> sessionAuthMap = new ConcurrentHashMap<>();

    @Override
    protected McpServerSession.Factory createSessionFactory(McpServer server) {
        McpServerSession.Factory originalFactory = super.createSessionFactory(server);
        return transport -> {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            McpServerSession session = originalFactory.create(transport);
            if (auth != null) {
                sessionAuthMap.put(session.getId(), auth);
            }
            // 包装会话,关闭时清理
            return new McpServerSession() {
                @Override
                public String getId() {
                    return session.getId();
                }

                @Override
                public void close() {
                    sessionAuthMap.remove(session.getId());
                    session.close();
                }

                @Override
                public Mono<Void> closeGracefully() {
                    sessionAuthMap.remove(session.getId());
                    return session.closeGracefully();
                }

                // 其他方法委托给session...
                @Override
                public void init(McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo) {
                    session.init(clientCapabilities, clientInfo);
                }

                @Override
                public <T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef) {
                    return session.sendRequest(method, requestParams, typeRef);
                }

                @Override
                public Mono<Void> sendNotification(String method, Object params) {
                    return session.sendNotification(method, params);
                }

                @Override
                public Mono<Void> handle(McpSchema.JSONRPCMessage message) {
                    return session.handle(message);
                }

                @Override
                public Mono<Void> sendNotification(String method) {
                    return session.sendNotification(method);
                }
            };
        };
    }

    public Optional<Authentication> getAuthForSession(String sessionId) {
        return Optional.ofNullable(sessionAuthMap.get(sessionId));
    }
}

工具回调中获取:

@Bean
public ToolCallback userSecrets(AuthStoringWebMvcSseServerTransportProvider transportProvider) {
    return FunctionToolCallback.builder("userSecrets", (UserSecretsInput input, ToolContext context) -> {
        String userName = McpToolUtils.getMcpExchange(context)
                .map(exchange -> exchange.getSession().getId())
                .flatMap(transportProvider::getAuthForSession)
                .map(Authentication::getName)
                .orElseThrow(() -> new RuntimeException("could not get username"));
        return "user specific data for " + userName + " with input: " + input;
    })
    .inputType(UserSecretsInput.class)
    .description("Provide user specific data")
    .build();
}

record UserSecretsInput(String input){};

方案对比

方案优势适用场景
Reactor Context传递完全符合响应式模型,无状态,无内存泄漏风险异步工具、响应式工具场景
自定义ToolExecutor直接控制线程上下文,兼容同步工具同步工具为主的场景
会话属性存储改动小,依赖公开API,兼容性好现有代码迁移、快速适配场景

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 07:07:04