MCP(模型上下文協議)是由 Anthropic 引入的開放標準,旨在讓 AI 模型以結構化的方式與外部工具、數據源和服務進行交互。一個 MCP 服務器是一個輕量級的後端應用程序,它通過 MCP 接口暴露特定的能力,例如訪問文件、查詢數據庫或調用 API。
為了使 MCP 服務器具備生產級能力,我們可能會考慮將其分離為獨立的應用程序。這有助於我們獨立地對其進行擴展和維護。然而,由於這些服務器可能會處理敏感任務,因此我們需要安全其端點並限制對受信任客户端的訪問。
這時 OAuth2 登場了。OAuth2 是一個用於安全、基於令牌的 API 訪問授權協議。而不是直接管理用户憑據,我們的 MCP 服務器信任由中心授權服務器頒發的驗證令牌。我們可以使用 OAuth2 來根據範圍和角色授予或限制客户端應用程序對特定 MCP 能力的訪問權限。
在本教程中,我們將學習如何使用 OAuth2 在 Spring AI 應用程序中安全地保護 MCP 服務器。
首先,我們添加 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 資源服務器 依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>3.4.2</version>
</dependency>
使用此依賴項,我們將確保我們的 MCP 端點拒絕無效或缺失的 Bearer 令牌。
েন্স>
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 類:
在這裏,我們允許所有授權請求訪問 /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. 結論
在本教程中,我們使用 OAuth2 在 Spring AI 應用程序中保護了我們的 MCP 服務器。為了保護關鍵的 MCP 端點,OAuth2 通過 Spring Boot 無縫集成。此外,此配置具有靈活性,並且可以進一步擴展。例如,我們可以引入基於角色和範圍的訪問控制,以限制特定工具或操作到某些客户端。
在生產環境中,我們可能會與功能齊全的身份提供程序(如 Keycloak 或 Okta)集成。此外,我們可以使用自定義聲明或範圍來增強我們的令牌,從而控制對 MCP 平台中單個工具的訪問。