知識庫 / Spring / Spring AI RSS 訂閱

Spring AI 和 OAuth2 授權機制

Artificial Intelligence,Spring AI,Spring Security
HongKong
8
10:42 AM · Dec 06 ,2025

1. 概述

模型上下文協議 (MCP) 允許人工智能模型通過安全的 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

Spring Boot 在我們設置 issuer URI 時會自動處理 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 依賴項:

org.springframework.ai/spring-ai-starter-mcp-client-webfluxorg.springframework.boot/spring-boot-starter-oauth2-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

整個挑戰的核心在於為每個請求選擇合適的 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

讓我們用 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界面發起請求時,它使用他們的授權碼令牌。

7. 驗證配置

為了理解應用程序的工作原理,我們需要查看其產生的結果。 在啓動任何應用程序之前,必須設置 LLM 所需的環境變量。 配置完成後,首先啓動授權服務器,該服務器監聽端口 9000。 這非常重要,因為所有其他模塊都依賴於授權服務器。 然後啓動 MCP 服務器,該服務器監聽端口 8090,最後啓動 MCP 客户端,該客户端監聽端口 8080。

測試完整流程變得簡單明瞭。 我們需要訪問 MCP 客户端端點,然後嘗試執行以下操作:

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

客户端將從授權服務器獲取一個令牌,然後通過該令牌調用 MCP 服務器,並返回計算結果。我們需要確保我們使用配置文件的指定憑據登錄到授權服務器。

8. 結論

在本教程中,我們探討了 OAuth2 如何通過基於令牌的授權為 MCP 系統提供強大的安全保障。Spring Security 的 OAuth2 支持能夠實現良好的保護,同時配置量極少。通過分離授權服務器、MCP 服務器和 MCP 客户端,我們構建了一個架構,其中每個組件專注於自身的職責,從而提供了靈活性。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.