MCP 最新企業級權限認證方案,STDIO-SSE 不同實現詳解!

在做 MCP 企業級方案落地時, 我們可能不想讓沒有權限的人訪問 MCP Server, 或者需要根據不同的用戶返回不同的數據, 這裏就涉及到 MCP Server 授權操作。

那 MCP Server 有 2 種傳輸方式, 實現起來不一樣:

STDIO

這種方式在本地運行, 它 將 MCP Server 作爲子進程啓動。 我們稱爲標準輸入輸出, 其實就是利用運行命令的方式寫入和讀取控制檯的信息,以達到傳輸。

通常我們會配置一段 json,比如這裏的百度地圖 MCP Server :

如果你傳入的 BAIDU_MAP_API_KEY 不對, 就沒有使用權限。

"baidu-map": {
  "command""cmd",
  "args": [
    "/c",
    "npx",
    "-y",
    "@baidumap/mcp-server-baidu-map"
  ],
  "env": {
    "BAIDU_MAP_API_KEY""LEyBQxG9UzR9C1GZ6zDHsFDVKvBem2do"
  }
},

所以 STDIO 做授權的方式很明確, 就是通過 env【環境變量】,實現步驟如下:

  1. 服務端發放一個用戶的憑證(可以是祕鑰、token) 這步不細講,需要有一個授權中心發放憑證。

  2. 通過 mcp client 通過 env 傳入憑證

  3. mcp server 通過環境變量鑑權

所以在 MCP Server 端就可以通過獲取環境變量的方式獲取 env 裏面的變量:

也可以通過 AOP 的方式統一處理

    @Tool(description = "獲取用戶餘額")
    public String getScore() {
        String userName = System.getenv("API_KEY"); 
        // todo .. 鑑權處理
        return "未檢索到當前用戶"+userName;
    }

:::danger 這種方式要注意: 他不支持動態鑑權, 也就是動態更換環境變量, 因爲 STDIO 是本地運行方式,**它 將 MCP Server 作爲子進程啓動, **如果是多個用戶動態切換憑證, 會對共享的環境變量造成爭搶, 最終只能存儲一個。 除非一個用戶對應一個 STDIO MCP Server. 但是這樣肯定很喫性能! 如果要多用戶動態切換授權, 可以用 SSE 的方式;

:::

SSE

說明

不過,如果你想把 MCP 服務器開放給外部使用,就需要暴露一些標準的 HTTP 接口。對於私有場景,MCP 服務器可能並不需要嚴格的身份認證,但在企業級部署下,對這些接口的安全和權限把控就非常重要了。爲了解決這個問題,2025 年 3 月發佈的最新 MCP 規範引入了安全基礎,藉助了廣泛使用的 OAuth2 框架。

本文不會詳細介紹 OAuth2 的所有內容,不過簡單回顧一下還是很有幫助。

在規範的草案中,MCP 服務器既是資源服務器,也是授權服務器。

curl https://mcp.example.com/sse -H "Authorization: Bearer <有效的 access token>"

用 Spring Security 和 Spring Authorization Server,可以方便地爲現有的 Spring MCP 服務器加上這兩大安全能力。

給 Spring MCP 服務器加上 OAuth2 支持

這裏以官方例子倉庫的【天氣】MCP 工具演示如何集成 OAuth2,主要是讓服務器端能簽發和校驗令牌。

首先,<font>pom.xml</font> 裏添加必要的依賴:

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

接着,在 <font>application.properties</font>配置里加上簡易的 OAuth2 客戶端信息,便於請求令牌:

spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-id=xushu
spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-secret={noop}xushu666
spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-authentication-methods=client_secret_basic
spring.security.oauth2.authorizationserver.client.oidc-client.registration.authorization-grant-types=client_credentials

這樣定義後,你可以直接通過 POST 請求和授權服務器交互,無需瀏覽器,用配置好的 <font>/secret</font> 作爲固定憑據。 比如 最後一步是開啓授權服務器和資源服務器功能。通常會新增一個安全配置類,比如 <font>SecurityConfiguration</font>,如下:

import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer.authorizationServer;

@Configuration
@EnableWebSecurity
class SecurityConfiguration {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .with(authorizationServer(), Customizer.withDefaults())
        .oauth2ResourceServer(resource -> resource.jwt(Customizer.withDefaults()))
        .csrf(CsrfConfigurer::disable)
        .cors(Customizer.withDefaults())
        .build();
    }
}

這個過濾鏈主要做了這些事情:

這樣配置之後,只有帶 access_token 的訪問纔會被接受,否則會直接返回 401 未授權錯誤,例如:

curl http://localhost:8080/sse --fail-with-body
# 返回:
# curl: (22) The requested URL returned error: 401

要使用 MCP 服務器,先要獲取一個 access_token。可通過 <font>client_credentials</font> 授權方式(用於機器到機器、服務賬號的場景):

curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
# 返回:
# {"access_token":"<YOUR-ACCESS-TOKEN>","token_type":"Bearer","expires_in":299}

把返回的 access_token 記下來(它一般以 “ey” 開頭),之後就可以用它來正常請求服務器了:

curl http://localhost:8080/sse -H"Authorization: Bearer YOUR_ACCESS_TOKEN"
# 服務器響應內容

你還可以直接在 MCP inspector 工具裏用這個 access_token。從菜單的 Authentication > Bearer 處粘貼令牌並連接即可。

爲 MCP Client 設置請求頭

目前, mcp 的 java sdk 沒有提供 api 直接調用, 經過徐庶老師研究源碼後, 你只能通過 2 種方式實現:

重寫源碼

擴展 mcp 的 sse 方式 java sdk 的源碼, 整個重寫一遍。 工作量較大, 並且我預計過不了多久, spring ai 和 mcp 協議都會更新這塊。 看你的緊急程度, 如果考慮整體擴展性維護性,可以整體重寫一遍:

提供一個重寫思路

重寫 McpSseClientProperties

MCPSse 客戶端屬性配置:新增請求頭字段

package org.springframework.ai.autoconfigure.mcp.client.properties;

@ConfigurationProperties("spring.ai.mcp.client.sse")
public class McpSseClientProperties {
    public static final String CONFIG_PREFIX = "spring.ai.mcp.client.sse";
    private final Map<String, SseParameters> connections = new HashMap();
    
    private final Map<String, String> headersMap = new HashMap<>();
    private String defaultHeaderName;
    private String defaultHeaderValue;
    private boolean enableCompression = false;
    private int connectionTimeout = 5000;

    public McpSseClientProperties() {
    }

    public Map<String, SseParameters> getConnections() {
        return this.connections;
    }

    public Map<String, String> getHeadersMap() {
        return this.headersMap;
    }

    public String getDefaultHeaderName() {
        return this.defaultHeaderName;
    }

    public void setDefaultHeaderName(String defaultHeaderName) {
        this.defaultHeaderName = defaultHeaderName;
    }

    public String getDefaultHeaderValue() {
        return this.defaultHeaderValue;
    }

    public void setDefaultHeaderValue(String defaultHeaderValue) {
        this.defaultHeaderValue = defaultHeaderValue;
    }

    public boolean isEnableCompression() {
        return this.enableCompression;
    }

    public void setEnableCompression(boolean enableCompression) {
        this.enableCompression = enableCompression;
    }

    public int getConnectionTimeout() {
        return this.connectionTimeout;
    }

    public void setConnectionTimeout(int connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    public static record SseParameters(String url) {
        public SseParameters(String url) {
            this.url = url;
        }

        public String url() {
            return this.url;
        }
    }
}

重寫 SseWebFluxTransportAutoConfiguration

自動裝配添加請求頭配置信息

package org.springframework.ai.autoconfigure.mcp.client;

@AutoConfiguration
@ConditionalOnClass({WebFluxSseClientTransport.class})
@EnableConfigurationProperties({McpSseClientProperties.class, McpClientCommonProperties.class})
@ConditionalOnProperty(
        prefix = "spring.ai.mcp.client",
        name = {"enabled"},
        havingValue = "true",
        matchIfMissing = true
)
public class SseWebFluxTransportAutoConfiguration {
    public SseWebFluxTransportAutoConfiguration() {
    }

    @Bean
    public List<NamedClientMcpTransport> webFluxClientTransports(McpSseClientProperties sseProperties, WebClient.Builder webClientBuilderTemplate, ObjectMapper objectMapper) {
        List<NamedClientMcpTransport> sseTransports = new ArrayList();
        Iterator var5 = sseProperties.getConnections().entrySet().iterator();
        Map<String, String> headersMap = sseProperties.getHeadersMap();
        while(var5.hasNext()) {
            Map.Entry<String, McpSseClientProperties.SseParameters> serverParameters = (Map.Entry)var5.next();
            WebClient.Builder webClientBuilder = webClientBuilderTemplate.clone()
                    .defaultHeaders(headers -> {
                        if (headersMap != null && !headersMap.isEmpty()) {
                            headersMap.forEach(headers::add);
                        }
                    })
                    .baseUrl(((McpSseClientProperties.SseParameters)serverParameters.getValue()).url());
            WebFluxSseClientTransport transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper);
            sseTransports.add(new NamedClientMcpTransport((String)serverParameters.getKey(), transport));
        }

        return sseTransports;
    }

    @Bean
    @ConditionalOnMissingBean
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }

    @Bean
    @ConditionalOnMissingBean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

使用:

設置 WebClientCustomizer

在用 Spring-ai-M8 版本的時候, 發現提供了 WebClientCustomizer 進行擴展。 可以嘗試:

  1. 根據用戶憑證進行授權
curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
  1. 根據授權後的 token 進行請求:
@Bean
public WebClientCustomizer webClientCustomizer() {
    // 認證 mcp server  /oauth?username:password   --> access_token
    return (builder) -> {
        builder.defaultHeader("Authorization","Bearer eyJraWQiOiIzYmMzMDRmZC02NzcyLTRkYTItODJiMy1hNTEwNGExMDBjNTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ4dXNodSIsImF1ZCI6Inh1c2h1IiwibmJmIjoxNzQ2NzE4MjE5LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE3NDY3MTg1MTksImlhdCI6MTc0NjcxODIxOSwianRpIjoiM2VhMzIyODctNTQ5NC00NWZlLThlZDItZGY1MjViNmIwNzkxIn0.Q-zWBZxa2CeFZo2YinenyaLb8KBMMua40X8YSs4n2fez7ODihtoVuCeJQnd2Q6qV2Pa8Z3cfH4QcMUuxMJ-_sLtZaSXpbCThH5q3KoQZ8C4MLJRTpuRqv4z1n7uLNXiVG2rya5hGwjTxu5qzHuBa2ri9pamRwmsjTz4vLHBJ1ILxDJcTkZUFuV1ExQJViewGt_7KMYcFqzGyRPiS4mm4wVvJTDjqcEGwMelu51L44K1DDYgt29vVLRVQEmnUtbBzePAxRqfw_HWJdhRSeQNiqRYCYhdAlPr3QZUFJa54GpuZn3CNyaXFoL7mENSR7wCYWx6wi--_REw6oaIfeSm-Xg");
    };
}

:::danger SSE 是支持動態切換 token 的, 因爲一個請求就是一個新的 http 請求, 不會出現多線程爭搶。

但是需要動態請求:

curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666 進行重新授權

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/cpeghpQxFOcuZ1qtMKq-OA