使用Spring AI保护MCP服务器的完整指南

本文详细介绍了如何使用Spring AI框架保护MCP服务器,包括OAuth2认证、Spring授权服务器配置以及API密钥安全方案。通过具体代码示例展示如何实现规范化的安全防护措施。

使用Spring AI保护MCP服务器

模型上下文协议(简称MCP)已经席卷了AI世界。如果您一直关注我们的博客,可能已经阅读过该主题的介绍文章《连接您的AI到一切:Spring AI的MCP启动器》。

MCP的安全方面一直在快速发展,规范的最新版本获得了生态系统越来越多的支持。为了满足Spring用户的需求,我们在Github上孵化了一个专门的项目:spring-ai-community/mcp-security。

本周,我们发布了首个版本,您现在可以将它们添加到基于Spring AI 1.1.x的应用程序中。在本文中,我们将探讨:

  • 使用OAuth2保护MCP服务器
  • 构建MCP兼容的Spring授权服务器
  • 使用API密钥代替OAuth2保护MCP服务器

使用OAuth 2保护MCP服务器

根据MCP规范的授权部分,通过HTTP暴露的MCP服务器必须使用OAuth 2访问令牌进行保护。对MCP服务器的任何调用都必须包含Authorization: Bearer <access_token>头部,其中访问令牌是从授权服务器(如Okta、Github等)代表用户获取的。

MCP服务器还必须明确公告其信任的授权服务器,以便MCP客户端可以动态发现它们,向授权服务器注册自身,并获取令牌。

首先,将所需的依赖项添加到您的项目中:

Maven:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependencies>
    <!-- Spring AI MCP starter -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <!-- MCP Security -->
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>mcp-server-security</artifactId>
        <version>0.0.2</version>
    </dependency>
    <!-- MCP Security dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
</dependencies>

Gradle:

1
2
3
implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
implementation("org.springaicommunity:mcp-server-security:0.0.2")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")

确保在您的application.properties中启用MCP服务器,并注入您的授权服务器URL:

1
2
3
4
5
6
spring.ai.mcp.server.name=my-cool-mcp-server
# Supported protocols: STREAMABLE, STATELESS
spring.ai.mcp.server.protocol=STREAMABLE
# Choose any property name you'd like

authorization.server.url=<AUTH_SERVER_URL>

我们将添加一个简单的MCP工具,根据输入语言(“english”、“french"等)和用户名向用户问候:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class MyToolsService {

    @McpTool(name = "greeter", description = "A tool that greets you, in the selected language")
    public String greet(
            @ToolParam(description = "The language for the greeting (example: english, french, ...)") String language
    ) {
        if (!StringUtils.hasText(language)) {
            language = "";
        }
        var authentication = SecurityContextHolder.getContext().getAuthentication();
        var name = authentication.getName();
        return switch (language.toLowerCase()) {
            case "english" -> "Hello, %s!".formatted(name);
            case "french" -> "Salut %s!".formatted(name);
            default -> ("I don't understand language \"%s\". " +
                        "So I'm just going to say Hello %s!").formatted(language, name);
        };
    }
}

在这个示例中,该工具将从SecurityContext中查找用户名,并创建个性化问候。用户名将来自用于验证请求的JWT访问令牌中的sub声明。

最后但同样重要的是,我们为安全添加一个配置类,例如McpServerSecurityConfiguration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@EnableWebSecurity
class McpServerSecurityConfiguration {

    @Value("${authorization.server.url}")
    private String authServerUrl;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                // Enforce authentication with token on EVERY request
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // Configure OAuth2 on the MCP server
                .with(
                        McpResourceServerConfigurer.mcpServerOAuth2(),
                        (mcpAuthorization) -> {

                            mcpAuthorization.authorizationServer(this.authServerUrl);
                            // OPTIONAL: enforce the `aud` claim in the JWT token.
                            mcpAuthorization.validateAudienceClaim(true);
                        }
                )
                .build();
    }
}

使用./mvnw spring-boot:run./gradlew bootRun运行应用程序。它应该在8080端口启动。

MCP兼容的Spring授权服务器

要使用Spring创建MCP兼容的授权服务器,请创建一个新的Spring项目,使用Spring Authorization Server,并添加MCP特定的依赖:

Maven

1
2
3
4
5
<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>mcp-authorization-server</artifactId>
    <version>0.0.2</version>
</dependency>

Gradle

1
implementation("org.springaicommunity:mcp-authorization-server:0.0.2")

您可以以通常的方式配置授权服务器。以下是一个注册默认客户端和默认用户的application.yml示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
spring:
  application:
    name: sample-authorization-server
  security:
    oauth2:
      authorizationserver:
        client:
          default-client:
            token:
              access-token-time-to-live: 1h
            registration:
              client-id: "default-client-id"
              client-secret: "{noop}default-client-secret"
              client-authentication-methods:
                - "client_secret_basic"
                - "none"
              authorization-grant-types:
                - "authorization_code"
                - "client_credentials"
              redirect-uris:
                - "http://127.0.0.1:8080/authorize/oauth2/code/authserver"
                - "http://localhost:8080/authorize/oauth2/code/authserver"
                # mcp-inspector
                - "http://localhost:6274/oauth/callback"
    user:
      # A single user, named "user"
      name: user
      password: password

server:
  port: 9000
  servlet:
    session:
      cookie:
        # Override the default cookie name (JSESSIONID).
        # This allows running multiple Spring apps on localhost, and they'll each have their own cookie.
        # Otherwise, since the cookies do not take the port into account, they are confused.
        name: MCP_AUTHORIZATION_SERVER_SESSIONID

然后,您可以使用通常的Spring Security API激活所有授权服务器功能,即安全过滤器链:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            // all requests must be authenticated
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            // enable authorization server customizations
            .with(McpAuthorizationServerConfigurer.mcpAuthorizationServer(), withDefaults())
            // enable form-based login, for user "user"/"password"
            .formLogin(withDefaults())
            .build();
}

超越OAuth 2:API密钥

虽然MCP规范要求使用OAuth2进行安全保护,但许多环境没有支持此用例的基础设施。为了在缺乏OAuth 2的环境中使用,许多客户端(包括MCP检查器本身)允许您在发出请求时传递自定义头部。

这为替代身份验证流程打开了大门,包括基于API密钥的安全保护。MCP安全项目支持API密钥,我们将在下面展示。

首先,将依赖项添加到您的项目中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependencies>
    <!-- Spring AI MCP starter -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <!-- MCP Security -->
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>mcp-server-security</artifactId>
        <version>0.0.2</version>
    </dependency>
    <!-- MCP Security dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

Gradle:

1
2
3
implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
implementation("org.springaicommunity:mcp-server-security:0.0.2")
implementation("org.springframework.boot:spring-boot-starter-security")

确保在您的application.properties中启用MCP服务器:

1
2
3
spring.ai.mcp.server.name=my-cool-mcp-server
# Supported protocols: STREAMABLE, STATELESS
spring.ai.mcp.server.protocol=STREAMABLE

通过API密钥验证的"实体”(如用户或服务账户)由ApiKeyEntity表示。MCP服务器检查特定头部中的API密钥,加载实体,并验证密钥。

然后,您可以使用通常的Spring Security方式为项目配置安全:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSecurity
class McpServerConfiguration {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
                .with(
                        McpApiKeyConfigurer.mcpServerApiKey(),
                        (apiKey) -> apiKey.apiKeyRepository(apiKeyRepository())
                )
                .build();
    }

    private ApiKeyEntityRepository<ApiKeyEntityImpl> apiKeyRepository() {
        var apiKey = ApiKeyEntityImpl.builder()
                .name("test api key")
                .id("api01")
                .secret("mycustomapikey")
                .build();

        return new InMemoryApiKeyEntityRepository<>(List.of(apiKey));
    }
}

在这里,我们使用存储简单密钥的API密钥存储库。然后,您应该能够使用头部X-API-key: api01.mycustomapikey调用您的MCP服务器。X-API-key是传递API密钥的默认头部名称,后跟头部值{id}.{secret}。密钥在服务器端以bcrypt哈希形式存储。

mcpServerApiKey()配置器提供了更改头部名称的选项,甚至提供了专用API来从传入的HTTP请求中提取API密钥。

改进MCP安全性

如果您想了解更多信息,请访问spring-ai-community/mcp-security项目,查看文档和示例。您还将找到使用Spring AI和Spring Security的客户端MCP安全支持。

在您的项目和应用程序中尝试它,与生态系统的其余部分一起测试它,并帮助我们改进它!我们欢迎贡献,包括反馈和问题。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计