使用Spring AI和OAuth2进行MCP授权

2025/09/30

1. 概述

模型上下文协议(MCP)允许AI模型通过安全的API访问业务数据,构建处理敏感信息的MCP服务器时,我们需要适当的授权来控制哪些人可以访问哪些数据。OAuth2提供了基于令牌的安全性,可以与MCP系统完美兼容。我们无需构建自定义身份验证,而是可以使用OAuth2标准来保护MCP服务器并管理客户端访问。

在本文中,我们将了解如何使用Spring AI和OAuth2保护MCP服务器和客户端的安全。我们将构建一个包含三个组件的完整示例:授权服务器、带有计算器工具的受保护MCP服务器,以及处理用户和系统请求的客户端。

2. MCP安全架构

为了确保MCP服务器的安全,了解如何在MCP服务器之前集成授权服务器非常重要。

我们的系统包含一个授权服务器,用于签发具有适当权限的JWT令牌;一个MCP服务器,用于验证令牌并控制对计算器工具的访问。此外,还有一个MCP客户端,用于获取令牌并管理不同请求类型的身份验证:

MCP服务器充当OAuth2资源服务器,它们在处理任何操作之前都会检查请求标头中的JWT令牌。这将安全问题与业务逻辑区分开来,客户端从OAuth2授权服务器获取访问令牌。然后,客户端将这些令牌包含在MCP请求中。最后,MCP服务器在允许操作之前验证令牌。

3. 构建授权服务器

我们将从授权服务器开始,因为其他组件都依赖于它。

3.1 添加依赖

我们需要添加OAuth2授权服务器依赖才能使用它:

<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>

3.2 配置授权服务器

让我们在application.yml中配置服务器:

server:
    port: 9000

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://localhost:8080/authorize/oauth2/code/authserver"
                            scopes:
                                - "openid"
                                - "profile"
                                - "calc.read"
                                - "calc.write"

这会在端口9000上设置一个授权服务器,其客户端支持授权码流(针对用户)和客户端凭证流(针对系统)

4. 保护MCP服务器

现在我们将创建一个需要OAuth2令牌并提供计算器工具的MCP服务器。

4.1 配置MCP服务器依赖

现在,让我们添加OAuth2资源服务器支持:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    <version>1.0.0-M7</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

4.2 服务器配置

让我们将MCP服务器配置为OAuth2资源服务器:

server.port=8090

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000

spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.name=mcp-calculator-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.stdio=false

当我们设置颁发者URI时,Spring Boot会自动处理JWT验证。现在,每个发送到MCP服务器的请求都需要在Authorization标头中携带有效的JWT令牌

4.3 创建MCP工具

LLM学生通常数学不好,因此,我们需要为他们提供一些工具,让他们能够根据需求计算结果:

@Tool(description = "Add two numbers")
public CalculationResult add(
        @ToolParam(description = "First number") double a,
        @ToolParam(description = "Second number") double b) {
    double result = a + b;
    return new CalculationResult("addition", a, b, result);
}

@Tool(description = "Multiply two numbers")
public CalculationResult multiply(
        @ToolParam(description = "First number") double a,
        @ToolParam(description = "Second number") double b) {
    double result = a * b;
    return new CalculationResult("multiplication", a, b, result);
}

安全配置会自动保护所有MCP工具,没有有效令牌的请求将被拒绝。这些工具会被添加到上下文中,并在每次用户查询时提供给LLM。然后,LLM会决定使用哪个工具来响应用户查询。

5. 构建MCP客户端

现在,我们需要客户端处理最复杂的部分,因为它需要处理用户请求和系统初始化。

5.1 客户端依赖

我们首先添加mcp-clientoauth2-client依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

5.2 客户端配置

现在,我们需要在application.properties中配置两个OAuth2客户端注册:

server.port=8080

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

# OAuth2 Client for User-Initiated Requests (Authorization Code Grant)
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
spring.security.oauth2.client.registration.authserver.scope=openid,profile,mcp.read,mcp.write
spring.security.oauth2.client.registration.authserver.redirect-uri={baseUrl}/authorize/oauth2/code/{registrationId}

# OAuth2 Client for Machine-to-Machine Requests (Client Credentials Grant)
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
spring.security.oauth2.client.registration.authserver-client-credentials.scope=mcp.read,mcp.write

spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}

我们需要两个注册来对应不同的身份验证流程,authserver注册使用授权码流程来处理用户发起的请求,authserver-client-credentials注册使用客户端凭据流程来处理系统启动。

5.3 安全配置

现在让我们设置Spring Security来处理OAuth2:

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

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

5.4 选择正确的Token

这里的全部挑战在于为每个请求选择正确的令牌,我们需要一个自定义的ExchangeFilterFunction实现来检测请求上下文:

@Component
public class McpSyncClientExchangeFilterFunction implements ExchangeFilterFunction {

    private final ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialTokenProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
    private final ServletOAuth2AuthorizedClientExchangeFilterFunction delegate;
    private final ClientRegistrationRepository clientRegistrationRepository;
    private static final String AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID = "authserver";
    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;
    }

    @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();
    }

    public Consumer<WebClient.Builder> configuration() {
        return builder -> builder.defaultRequest(this.delegate.defaultRequest()).filter(this);
    }
}

过滤器会检查是否存在活动的Web请求,如果存在,则使用用户的授权码令牌;如果不存在(例如在应用启动时),则使用客户端凭据。

6. 使用安全的MCP系统

现在我们已经涵盖了所有组件,让我们看看如何有效地使用安全的MCP系统。

6.1 创建聊天客户端

让我们用ChatClient将所有内容连接在一起:

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

6.2 发起请求

现在我们可以正常使用ChatClient了,安全机制会自动生效:

@GetMapping("/calculate")
public String calculate(@RequestParam String expression, @RegisteredOAuth2AuthorizedClient("authserver") OAuth2AuthorizedClient authorizedClient) {
    String prompt = String.format("Please calculate the following mathematical expression using the available calculator tools: %s", expression);

    return chatClient.prompt()
        .user(prompt)
        .call()
        .content();
}

在启动过程中,MCP客户端初始化使用客户端凭证令牌。当用户通过Web界面发出请求时,MCP客户端会使用其授权码令牌。

7. 验证设置

为了理解该应用程序的工作原理,我们需要研究它产生的结果。在启动任何应用程序之前,我们必须为LLM设置所需的环境变量。设置完成后,我们首先在端口9000上启动授权服务器。这一点很重要,因为所有其他模块都依赖于授权服务器。然后,我们在端口8090上启动MCP服务器,接着在端口8080上启动MCP客户端。

测试整个流程变得简单,我们需要访问MCP客户端,然后尝试以下操作:

http://{base_url}:8080/calculate?expression=15+25

客户端将从授权服务器获取令牌,使用该令牌调用MCP服务器,并返回计算结果,我们必须确保使用配置文件中指定的凭据登录授权服务器。

8. 总结

在本教程中,我们探讨了OAuth2如何通过基于标准令牌的授权为MCP系统提供强大的安全性。Spring Security的OAuth2支持能够以最少的配置提供良好的保护。通过分离授权服务器、MCP服务器和MCP客户端,我们创建了一个架构,每个组件都专注于各自的职责,从而提高了灵活性。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章