1. 简介

MCP (Model Context Protocol) 是 Anthropic 推出的开放标准,旨在让 AI 模型以结构化方式与外部工具、数据源和服务交互。MCP 服务器是轻量级后端应用,通过 MCP 接口暴露特定能力,如访问文件、查询数据库或调用 API。

要让 MCP 服务器投入生产,通常需要将其拆分为独立应用。这种拆分便于独立扩展和维护。但由于这些服务器可能处理敏感任务,必须保护其接口并限制仅受信任客户端访问。

这时就需要 OAuth2 登场。OAuth2 是成熟的 API 安全访问委托协议,基于令牌进行授权。我们的 MCP 服务器无需直接管理用户凭证,而是信任中央授权服务器颁发的有效访问令牌。通过 OAuth2,可根据作用域(scope)和角色控制客户端应用对特定 MCP 能力的访问权限。

本教程将学习如何在 Spring AI 应用中使用 OAuth2 保护 MCP 服务器。

2. 依赖项

首先添加 Spring AI MCP 服务器 依赖,用于获取 HTTP/SSE 传输层和核心 MCP 支持:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
</dependency>

接着添加 OAuth 授权服务器 依赖,用于颁发 OAuth2 访问令牌:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    <version>3.3.3</version>
</dependency>

最后添加 Spring Security 资源服务器 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    <version>3.4.2</version>
</dependency>

此依赖确保 MCP 接口拒绝无效或缺失的 Bearer 令牌。

3. 创建股票信息 MCP 服务器

现在实现一个简单的 MCP 服务器,包含一个返回指定股票代码价格的工具

创建 StockInformationHolder 类:

public class StockInformationHolder {
    @Tool(description = "Get stock price for a company symbol")
    public String getStockPrice(@ToolParam String symbol) {
        if ("AAPL".equalsIgnoreCase(symbol)) {
            return "AAPL: $150.00";
        } else if ("GOOGL".equalsIgnoreCase(symbol)) {
            return "GOOGL: $2800.00";
        } else {
            return symbol + ": Data not available";
        }
    }
}

getStockPrice() 方法返回已知公司股价,未知代码返回默认响应。该方法用 @Tool 注解标记,用于构建工具定义;symbol 参数用 @ToolParam 注解标记,确保在工具定义构建过程中被正确处理。

接下来创建 McpServerConfiguration 类:

@Configuration
public class McpServerConfiguration {

    @Bean
    public ToolCallbackProvider stockTools() {
        return MethodToolCallbackProvider
          .builder()
          .toolObjects(new StockInformationHolder())
          .build();
    }
}

这里提供了 ToolCallbackProvider Bean,通过附加 StockInformationHolder 类构建。现在已拥有可用的 MCP 服务器,启动应用后调用 GET /sse 接口即可建立 SSE 连接。 要向 MCP 服务器发送消息,使用 POST /mcp/message 接口并附带 JSON 请求体:

{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "tools/call",
  "params": {
    "name": "getStockPrice",
    "arguments": {
      "arg0": "AAPL"
    }
  }
}

method 指定为 "tools/call" 表示要调用工具功能。params 对象包含参数:工具 name(默认为注解方法名)和 arguments 映射。

4. 添加安全配置

现在保护 MCP 服务器。首先通过 application.yml 配置授权服务器:

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          oidc-client:
            registration:
              client-id: mcp-client
              client-secret: "{noop}secret"
              client-authentication-methods: client_secret_basic
              authorization-grant-types: client_credentials

指定了请求令牌的客户端唯一标识符。共享密钥使用 {noop}secret(仅适用于演示),{noop} 前缀告知 Spring 不对密钥哈希,便于测试。

接下来创建 McpServerSecurityConfiguration 类:

@Configuration
@EnableWebSecurity
public class McpServerSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
          .authorizeHttpRequests(auth -> auth
            .requestMatchers("/mcp/**").authenticated()
            .requestMatchers("/sse").authenticated()
            .anyRequest().permitAll())
          .with(OAuth2AuthorizationServerConfigurer.authorizationServer(), Customizer.withDefaults())
          .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
          .csrf(CsrfConfigurer::disable)
          .cors(Customizer.withDefaults())
          .build();
    }
}

允许所有已认证请求访问 /mcp/sse 接口,其他接口保持开放。此方式简化了认证接口访问,但在生产环境中应更严格地限制访问。

使用 authorizationServer()oauth2ResourceServer() 方法配置应用。 此设置表明应用提供令牌端点,同时作为资源服务器通过 JWT 令牌验证传入请求。

5. 测试受保护的 MCP 服务器

现在测试受保护的 MCP 服务器。创建 McpServerOAuth2LiveTest 类:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class McpServerOAuth2LiveTest {

    private static final Logger log = LoggerFactory.getLogger(McpServerOAuth2LiveTest.class);

    @LocalServerPort
    private int port;

    private WebClient webClient;

    @BeforeEach
    void setup() {
        webClient = WebClient.create("http://localhost:" + port);
    }
}

在随机端口启动应用并初始化 WebClient。然后调用 /sse 接口建立服务器发送事件连接:

Flux<String> eventStream = webClient.get()
  .uri("/sse")
  .header("Authorization", obtainAccessToken())
  .accept(MediaType.TEXT_EVENT_STREAM)
  .retrieve()
  .bodyToFlux(String.class);

eventStream.subscribe(
    data -> {
        log.info("Response received: {}", data);
        if (!isRequestMessage(data)) {
            assertThat(data).containsSequence("AAPL", "$150");
        }
    },
    error -> log.error("Stream error: {}", error.getMessage()),
    () -> log.info("Stream completed")
);

断言响应消息包含预期数据。接着向 /mcp/message 接口发送请求:

Flux<String> sendMessage = webClient.post()
  .uri("/mcp/message")
  .header("Authorization", obtainAccessToken())
  .contentType(MediaType.APPLICATION_JSON)
  .accept(MediaType.TEXT_EVENT_STREAM)
  .bodyValue("""
     {
         "jsonrpc": "2.0",
         "id": "1",
         "method": "tools/call",
         "params": {
             "name": "getStockPrice",
             "arguments": {
                 "arg0": "AAPL"
             }
         }
     }
     """)
  .retrieve()
  .bodyToFlux(String.class);

发送请求获取 AAPL 股价。两个请求都包含 Authorization 头部。现在实现获取访问令牌的方法:

public String obtainAccessToken() {
    String clientId = "mcp-client";
    String clientSecret = "secret";
    String basicToken = Base64.getEncoder()
      .encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8));

    return "Bearer " + webClient.post()
      .uri("/oauth2/token")
      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
      .header(HttpHeaders.AUTHORIZATION, "Basic " + basicToken)
      .body(BodyInserters.fromFormData("grant_type", "client_credentials"))
      .retrieve()
      .bodyToMono(JsonNode.class)
      .map(node -> node.get("access_token").asText())
      .block(Duration.ofSeconds(5));
}

执行后可看到成功接收响应数据,表明已通过安全过滤器验证。

6. 结论

本教程学习了如何在 Spring AI 应用中使用 OAuth2 保护 MCP 服务器。通过 Spring Boot 无缝集成 OAuth2 保护了关键 MCP 接口。此配置具有灵活性,可进一步扩展,例如引入基于角色和作用域的访问控制,限制特定客户端访问工具或操作。

生产环境中可集成功能完备的身份提供商(如 KeycloakOkta)。此外,可通过自定义声明(claim)或作用域增强令牌,控制对 MCP 平台内单个工具的访问权限。

代码示例见 GitHub 仓库


原始标题:Securing Spring AI MCP Servers With OAuth2 | Baeldung