領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多上個月,我們探討了如何使用 OAuth2 授權框架來保護 Spring AI MCP 伺服器[1]。在那篇文章的結論中,我們提到將探索使用獨立的授權伺服器進行 MCP 安全,並偏離當時的規範。
自從我們釋出這篇文章以來,社群一直在積極修訂規範的原始版本。 新草案更簡單,並且主要更改確實符合我們對安全的設想。MCP 伺服器仍然是 OAuth2 資源伺服器,這意味著它們使用透過標頭傳遞的訪問令牌來授權傳入請求。但是,它們本身不需要是授權伺服器:訪問令牌現在可以由外部授權伺服器頒發。
在這篇博文中,我們將描述如何在 MCP 伺服器中實現最新的規範修訂,以及如何保護您的 MCP 客戶端。
請隨意檢視之前的博文,以回顧 OAuth2 和 MCP。
在此示例中,我們將為示例 MCP 伺服器新增 OAuth 2 支援——來自我們 Spring AI 示例儲存庫的 “天氣” MCP 工具。
首先,我們在 pom.xml 中匯入所需的 Boot 啟動器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
然後,我們透過更新 application.properties 將 MCP 伺服器配置為 OAuth2 資源伺服器。
# Update the port so it does not clash with our Client application
server.port=8090
# Turn on OAuth2 Resource Server
# This assumes we have an Authorization Server running at https://:9000
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://:9000
感謝 Spring Security 和 Spring Boot 的支援,我們的 MCP 伺服器現在已完全受保護:每個請求都需要在 Authorization 標頭中包含 JWT 令牌。
如果您想了解有關 Spring Security 中 OAuth2 資源伺服器支援的更多資訊,請參閱 參考文件。
我們的 MCP 伺服器現在期望授權伺服器在 https://:9000 執行。在企業場景中,授權伺服器通常已透過雲服務或本地部署的 Keycloak 等伺服器提供。對於此演示,您可以使用我們隨演示提供的授權伺服器,並使用 ./mvnw spring-boot:run 執行它。
或者,您只需幾行配置即可構建自己的。首先,我們需要依賴項
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
然後,Spring Boot 將在 application.yml 中獲取一些配置。
server:
port: 9000
# Cookies are per-domain, multiple apps running on localhost on different ports share cookies.
# This can create conflicts. We ensure the session cookie is different from the cookie that
# the client application uses.
servlet:
session:
cookie:
name: MCP_AUTHSERVER_SESSION
spring:
security:
# Provide a default "user"
user:
name: user
password: password
# Configure the Authorization Server
oauth2:
authorizationserver:
client:
oidc-client:
registration:
client-id: "mcp-client"
client-secret: "{noop}mcp-secret"
client-authentication-methods:
- "client_secret_basic"
authorization-grant-types:
- "authorization_code"
- "client_credentials"
- "refresh_token"
redirect-uris:
# The client application can technically run on any port
- "http://127.0.0.1:8080/authorize/oauth2/code/authserver"
- "https://:8080/authorize/oauth2/code/authserver"
如果您想了解有關 Spring 中 OAuth2 授權伺服器支援的更多資訊,請參閱參考文件。
MCP 伺服器和授權伺服器設定簡單,配置直接。我們需要多做一些工作來保護 MCP 客戶端。要開始構建 MCP 客戶端,無論是否授權,請參閱參考文件。
⚠️ 目前,Spring AI 僅支援使用 WebClient 為 SYNC MCP 客戶端新增安全性。
確保您的應用程式具有正確的依賴項。
<!-- Use Spring WebMVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Use WebClient-based MCP-client -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<!-- Bring in Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
然後更新您的 application.properties。
# Configure MCP
spring.ai.mcp.client.sse.connections.server1.url=https://:8090
spring.ai.mcp.client.type=SYNC
# Authserver common config
spring.security.oauth2.client.provider.authserver.issuer-uri=https://:9000
# Security: for getting tokens used when calling MCP tools
spring.security.oauth2.client.registration.authserver.client-id=mcp-client
spring.security.oauth2.client.registration.authserver.client-secret=mcp-secret
spring.security.oauth2.client.registration.authserver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authserver.provider=authserver
# Security: for getting tokens used when listing tools, initializing, etc.
spring.security.oauth2.client.registration.authserver-client-credentials.client-id=mcp-client
spring.security.oauth2.client.registration.authserver-client-credentials.client-secret=mcp-secret
spring.security.oauth2.client.registration.authserver-client-credentials.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.authserver-client-credentials.provider=authserver
請注意,這裡我們註冊了兩個 OAuth2 客戶端。第一個使用 client_credentials 授權,用於初始化我們的客戶端應用程式。它允許使用機器到機器通訊設定與 MCP 客戶端的會話,以及列出可用工具:該流程中不涉及使用者。第二個使用 authorization_code 授權,並允許我們的應用程式代表終端使用者獲取令牌。該客戶端用於呼叫工具。
雖然這裡沒有解釋,但您需要將您選擇的 LLM 模型新增到您的應用程式中,以使其完整。
下一步是為 Spring AI 配置 MCP 客戶端,透過提供一個 @Bean。
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpClients) {
return chatClientBuilder.defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpClients)).build();
}
要將 OAuth2 新增到我們的 MCP 客戶端,我們配置一個 Spring Security SecurityFilterChain 來啟用 OAuth2,以及一個 MCP 客戶端使用的自定義 WebClient.Builder。
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.oauth2Client(Customizer.withDefaults())
.csrf(CsrfConfigurer::disable)
.build();
}
/**
* Overload Boot's default {@link WebClient.Builder}, so that we can inject an
* oauth2-enabled {@link ExchangeFilterFunction} that adds OAuth2 tokens to requests
* sent to the MCP server.
*/
@Bean
WebClient.Builder webClientBuilder(McpSyncClientExchangeFilterFunction filterFunction) {
return WebClient.builder().apply(filterFunction.configuration());
}
要將令牌新增到 MCP 客戶端請求中,我們需要一個自定義的 ExchangeFilterFunction,它根據上下文(使用者互動或應用程式初始化)決定使用哪個 OAuth2 令牌。對於 Spring Security 初學者來說,這可能有點令人困惑,但請隨意按原樣使用它。
/**
* A wrapper around Spring Security's
* {@link ServletOAuth2AuthorizedClientExchangeFilterFunction}, which adds OAuth2
* {@code access_token}s to requests sent to the MCP server.
* <p>
* The end goal is to use access_token that represent the end-user's permissions. Those
* tokens are obtained using the {@code authorization_code} OAuth2 flow, but it requires a
* user to be present and using their browser.
* <p>
* By default, the MCP tools are initialized on app startup, so some requests to the MCP
* server happen, to establish the session (/sse), and to send the {@code initialize} and
* e.g. {@code tools/list} requests. For this to work, we need an access_token, but we
* cannot get one using the authorization_code flow (no user is present). Instead, we rely
* on the OAuth2 {@code client_credentials} flow for machine-to-machine communication.
*/
@Component
public class McpSyncClientExchangeFilterFunction implements ExchangeFilterFunction {
private final ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialTokenProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
private final ServletOAuth2AuthorizedClientExchangeFilterFunction delegate;
private final ClientRegistrationRepository clientRegistrationRepository;
// Must match registration id in property
// spring.security.oauth2.client.registration.<REGISTRATION-ID>.authorization-grant-type=authorization_code
private static final String AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID = "authserver";
// Must match registration id in property
// spring.security.oauth2.client.registration.<REGISTRATION-ID>.authorization-grant-type=client_credentials
private static final String CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID = "authserver-client-credentials";
public McpSyncClientExchangeFilterFunction(OAuth2AuthorizedClientManager clientManager,
ClientRegistrationRepository clientRegistrationRepository) {
this.delegate = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientManager);
this.delegate.setDefaultClientRegistrationId(AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID);
this.clientRegistrationRepository = clientRegistrationRepository;
}
/**
* Add an {@code access_token} to the request sent to the MCP server.
* <p>
* If we are in the context of a ServletRequest, this means a user is currently
* involved, and we should add a token on behalf of the user, using the
* {@code authorization_code} grant. This typically happens when doing an MCP
* {@code tools/call}.
* <p>
* If we are NOT in the context of a ServletRequest, this means we are in the startup
* phases of the application, where the MCP client is initialized. We use the
* {@code client_credentials} grant in that case, and add a token on behalf of the
* application itself.
*/
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) {
return this.delegate.filter(request, next);
}
else {
var accessToken = getClientCredentialsAccessToken();
var requestWithToken = ClientRequest.from(request)
.headers(headers -> headers.setBearerAuth(accessToken))
.build();
return next.exchange(requestWithToken);
}
}
private String getClientCredentialsAccessToken() {
var clientRegistration = this.clientRegistrationRepository
.findByRegistrationId(CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID);
var authRequest = OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
.principal(new AnonymousAuthenticationToken("client-credentials-client", "client-credentials-client",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")))
.build();
return this.clientCredentialTokenProvider.authorize(authRequest).getAccessToken().getTokenValue();
}
/**
* Configure a {@link WebClient} to use this exchange filter function.
*/
public Consumer<WebClient.Builder> configuration() {
return builder -> builder.defaultRequest(this.delegate.defaultRequest()).filter(this);
}
}
有了這些,我們就擁有了所需的一切!向我們的 LLM 提出與天氣相關的問題將觸發呼叫我們的天氣 MCP 工具。
var chatResponse = chatClient.prompt("What is the weather in %s right now?".formatted(query))
.call()
.content();
如果您想親自嘗試,我們提供了一個完整打包的演示應用程式,可在 GitHub 上獲取。
這是實現完整端到端授權的第一步。透過利用 Spring 強大的可擴充套件性,我們可以將 OAuth2 新增到我們的 MCP 客戶端和伺服器中,但這需要編寫一些程式碼。
Spring 團隊正在努力構建更簡單的整合,並提供愉快的配置驅動的 Boot 使用者體驗。
我們還在為 MCP 伺服器開發細粒度許可權。在更高階的用例中,並非 MCP 伺服器中的所有工具/資源/提示都需要相同的許可權:“thing-reader”工具將對所有使用者可用,但“thing-writer”僅對管理員可用。
[1]: 模型上下文協議,簡稱 MCP,是一種允許 AI 模型以結構化方式與外部工具和資源互動並訪問它們的協議。Spring AI 為 MCP 伺服器和 MCP 客戶端提供開箱即用的支援。