使用 OAuth2 保護 Spring AI MCP 伺服器
1. 簡介
MCP(模型情境協定)是由 Anthropic 推出的開放標準,旨在讓 AI 模型能夠以結構化的方式與外部工具、資料來源和服務進行互動。 MCP 伺服器是一個輕量級的後端應用程序,它透過 MCP 介面公開特定功能,例如存取檔案、查詢資料庫或呼叫 API。
為了使 MCP 伺服器能夠滿足生產環境的需求,我們可以考慮將它們分割成獨立的應用程式。這有助於我們單獨擴展和維護它們。但是,由於這些伺服器可能處理敏感任務,我們需要保護它們的端點,並限制對受信任客戶端的存取。
這就是 OAuth2 的用武之地。 OAuth2 是一個著名的協議,用於安全地基於令牌委託 API 存取。我們的 MCP 伺服器不會直接管理使用者憑證,而是信任由中央授權伺服器所頒發的經過驗證的存取權杖。我們可以使用 OAuth2 根據範圍和角色授予或限制客戶端應用程式對特定 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
註解,因此將用於建構工具定義。此外,我們也用@ToolParam
註解標記了symbol
參數,以確保在工具定義建置過程中會考慮到它。
接下來,讓我們建立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 伺服器。為了保護關鍵的 MCP 端點,我們透過 Spring Boot 無縫整合了 OAuth2。此外,此設定非常靈活,可以進一步擴展。例如,我們可以引入基於角色和範圍的存取控制,將特定工具或操作限制給特定的客戶端。
在生產環境中,我們可能會與 Keycloak 或 Okta 等功能齊全的身分提供者整合。此外,我們還可以使用自訂聲明或範圍來增強令牌,以控制對 MCP 平台內各個工具的存取。
與往常一樣,程式碼可在 GitHub 上取得。