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 接口。此配置具有灵活性,可进一步扩展,例如引入基于角色和作用域的访问控制,限制特定客户端访问工具或操作。
生产环境中可集成功能完备的身份提供商(如 Keycloak 或 Okta)。此外,可通过自定义声明(claim)或作用域增强令牌,控制对 MCP 平台内单个工具的访问权限。
代码示例见 GitHub 仓库。