MCP 最新企業級權限認證方案,STDIO-SSE 不同實現詳解!
在做 MCP 企業級方案落地時, 我們可能不想讓沒有權限的人訪問 MCP Server, 或者需要根據不同的用戶返回不同的數據, 這裏就涉及到 MCP Server 授權操作。
那 MCP Server 有 2 種傳輸方式, 實現起來不一樣:
STDIO
這種方式在本地運行, 它 將 MCP Server 作爲子進程啓動。 我們稱爲標準輸入輸出, 其實就是利用運行命令的方式寫入和讀取控制檯的信息,以達到傳輸。
通常我們會配置一段 json,比如這裏的百度地圖 MCP Server :
-
其中 command 和 args 代表運行的命令和參數。
-
其實 env 中的節點 BAIDU_MAP_API_KEY 就是做授權的。
如果你傳入的 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【環境變量】,實現步驟如下:
-
服務端發放一個用戶的憑證(可以是祕鑰、token) 這步不細講,需要有一個授權中心發放憑證。
-
通過 mcp client 通過 env 傳入憑證
-
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 服務器既是資源服務器,也是授權服務器。
- 作爲資源服務器,MCP 負責檢查每個請求中的
<font>Authorization</font>
請求頭。這個請求頭必須包括一個 OAuth2<font>access_token</font>
(令牌),它代表客戶端的 “權限”。這個令牌通常是一個 JWT(JSON Web Token),也可能只是一個不可讀的隨機字符串。如果令牌缺失或無效(無法解析、已過期、不是發給本服務器的等),請求會被拒絕。正常情況下,調用示例如下:
curl https://mcp.example.com/sse -H "Authorization: Bearer <有效的 access token>"
- 作爲授權服務器,MCP 還需要有能力爲客戶端安全地簽發
<font>access_token</font>
。在發放令牌前,服務器會校驗客戶端的憑據,有時還需要校驗訪問用戶的身份。授權服務器決定令牌的有效期、權限範圍、目標受衆等特性。
用 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();
}
}
這個過濾鏈主要做了這些事情:
-
要求所有請求都要經過身份認證。也就是訪問 MCP 的接口,必須帶上 access_token。
-
同時啓用了授權服務器和資源服務器兩大能力。
-
關閉了 CSRF(跨站請求僞造防護),因爲 MCP 不是給瀏覽器直接用的,這部分無需開啓。
-
打開了 CORS(跨域資源共享),方便用 MCP inspector 測試。
這樣配置之後,只有帶 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 進行擴展。 可以嘗試:
- 根據用戶憑證進行授權
curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
- 根據授權後的 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