使用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中提取,完全符合响应式编程模型。
实现步骤
- 自定义会话工厂包装类,创建会话时将当前Authentication存入Reactor Context
- 工具执行时通过
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,执行完成后清理上下文,适合同步工具场景。
实现步骤
- 实现
ToolExecutor接口,在执行逻辑中注入Authentication - 注册自定义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关联存储,避免依赖内部类反射。
实现步骤
- 自定义
WebMvcSseServerTransportProvider,覆盖会话创建逻辑存储Authentication - 工具回调中通过公开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

