领先一步
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 客户端提供开箱即用的支持。