上个月,我们探讨了如何使用 OAuth2 授权框架保护 Spring AI MCP 服务器[1]。在那篇文章的结尾,我们提到将探索使用独立授权服务器(Authorization Server)来实现 MCP 安全,并偏离当时的规范。

自那之后,社区对原始规范进行了积极修订。新草案 更加简洁,主要变化也正如我们设想的安全方式。MCP 服务器依然是 OAuth2 资源服务器(Resource Server),即通过请求头中的 access token 进行授权。但它们不再需要自己实现授权服务器:access token 现在可以由外部授权服务器颁发。

本文将介绍如何在 MCP 服务器中实现最新规范,并如何保护你的 MCP 客户端。

保护 MCP 服务器

以 Spring AI 示例仓库中的 “Weather” MCP 工具 为例,添加 OAuth2 支持。

首先,在 pom.xml 中引入所需依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

然后,在 application.properties 配置 MCP 服务器为 OAuth2 资源服务器:

# 避免端口冲突
server.port=8090

# 启用 OAuth2 资源服务器
# 假设授权服务器运行在 http://localhost:9000
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000

得益于 Spring Security 和 Spring Boot 的支持,MCP 服务器现已完全受保护:每个请求都需要在 Authorization 头中携带 JWT token。

更多 OAuth2 资源服务器支持内容可参考 官方文档。

MCP 服务器现在需要一个运行在 http://localhost:9000 的授权服务器。在企业场景下,通常已有授权服务器(如 Keycloak)或云服务可用。演示中可直接使用官方 demo 提供的授权服务器,通过 ./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>

然后在 application.yml 中添加如下配置:

server:
  port:9000
servlet:
    session:
      cookie:
        name:MCP_AUTHSERVER_SESSION
spring:
security:
    user:
      name:user
      password:password
    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:
                -"http://127.0.0.1:8080/authorize/oauth2/code/authserver"
                -"http://localhost:8080/authorize/oauth2/code/authserver"

更多 Spring 授权服务器内容可参考 官方文档

构建 MCP 客户端

MCP 服务器和授权服务器配置都很简单。MCP 客户端的安全配置则稍复杂。无论是否需要授权,构建 MCP 客户端可参考官方文档。

⚠️ 目前 Spring AI 仅支持为 SYNC 类型的 MCP 客户端添加安全性,且需使用 WebClient

确保你的应用引入如下依赖:

<!-- Spring WebMVC -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebClient 版 MCP 客户端 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

然后在 application.properties 配置:

# MCP 配置
spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8090
spring.ai.mcp.client.type=SYNC

# 授权服务器通用配置
spring.security.oauth2.client.provider.authserver.issuer-uri=http://localhost:9000

# 用于调用 MCP 工具的 token
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

# 用于工具列表、初始化等的 token
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)用于初始化客户端应用、会话建立、工具列表等,属于机器到机器的通信。

  • 第二个(authorization_code)用于代表终端用户获取 token,调用工具时使用。

(此处未详细说明 LLM 模型集成,需参考 LLM 官方文档)

接下来为 Spring AI 配置 MCP 客户端,只需提供一个 @Bean

@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpClients) {
    return chatClientBuilder.defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpClients)).build();
}

为 MCP 客户端添加 OAuth2,只需配置 Spring Security 的 SecurityFilterChain 及自定义的 WebClient.Builder

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
        .oauth2Client(Customizer.withDefaults())
        .csrf(CsrfConfigurer::disable)
        .build();
}

@Bean
WebClient.Builder webClientBuilder(McpSyncClientExchangeFilterFunction filterFunction) {
    return WebClient.builder().apply(filterFunction.configuration());
}

为 MCP 客户端请求添加 token,需要自定义 ExchangeFilterFunction,根据上下文(用户交互或应用初始化)决定使用哪种 OAuth2 token。如下:

@Component
publicclass McpSyncClientExchangeFilterFunction implements ExchangeFilterFunction {
privatefinal ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialTokenProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
privatefinal ServletOAuth2AuthorizedClientExchangeFilterFunction delegate;
privatefinal ClientRegistrationRepository clientRegistrationRepository;
privatestaticfinal String AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID = "authserver";
privatestaticfinal 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;
  }
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
    if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) {
      returnthis.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();
    returnthis.clientCredentialTokenProvider.authorize(authRequest).getAccessToken().getTokenValue();
  }
public Consumer<WebClient.Builder> configuration() {
    return builder -> builder.defaultRequest(this.delegate.defaultRequest()).filter(this);
  }
}

这样一来,所有准备就绪!向 LLM 提问天气时会自动调用 Weather MCP 工具:

var chatResponse = chatClient.prompt("What is the weather in %s right now?".formatted(query))
        .call()
        .content();

如需体验完整 demo,可参考 官方示例仓库。

展望

这只是端到端授权实现的第一步。借助 Spring 的强大扩展性,我们可以为 MCP 客户端和服务器添加 OAuth2,但目前仍需编写部分代码。

Spring 团队正在努力打造更简单的集成方式,未来将实现更优雅的配置驱动体验。

我们也在开发 MCP 服务器的细粒度权限控制。在更复杂的场景下,并非所有工具/资源/提示都需要相同权限,比如“thing-reader”工具对所有用户开放,而“thing-writer”仅限管理员。


[1]: Model Context Protocol(简称 MCP),是一种让 AI 模型以结构化方式访问外部工具和资源的协议。Spring AI 为 MCP 服务器和客户端都提供了开箱即用的支持。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐