微信扫码
添加专属顾问
我要投稿
深入了解MCP企业级权限认证方案,掌握STDIO/SSE两种实现方式。 核心内容: 1. MCP Server授权操作的必要性和重要性 2. STDIO方式的实现步骤与授权机制详解 3. SSE方式对多用户动态授权的支持及其优势
在做MCP企业级方案落地时, 我们可能不想让没有权限的人访问MCP Server, 或者需要根据不同的用户返回不同的数据, 这里就涉及到MCP Server授权操作。
那MCP Server有2种传输方式, 实现起来不一样:
这种方式在本地运行,它 将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【环境变量】,实现步骤如下:
所以在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的方式;
:::
不过,如果你想把 MCP 服务器开放给外部使用,就需要暴露一些标准的 HTTP 接口。对于私有场景,MCP 服务器可能并不需要严格的身份认证,但在企业级部署下,对这些接口的安全和权限把控就非常重要了。为了解决这个问题,2025 年 3 月发布的最新 MCP 规范引入了安全基础,借助了广泛使用的 OAuth2 框架。
本文不会详细介绍 OAuth2 的所有内容,不过简单回顾一下还是很有帮助。
在规范的草案中,MCP 服务器既是资源服务器,也是授权服务器。
<font style="color:rgb(30, 107, 184);">Authorization</font>
请求头。这个请求头必须包括一个 OAuth2<font style="color:rgb(30, 107, 184);">access_token</font>
(令牌),它代表客户端的“权限”。这个令牌通常是一个 JWT(JSON Web Token),也可能只是一个不可读的随机字符串。如果令牌缺失或无效(无法解析、已过期、不是发给本服务器的等),请求会被拒绝。正常情况下,调用示例如下:curl https://mcp.example.com/sse -H "Authorization: Bearer <有效的 access token>"
<font style="color:rgb(30, 107, 184);">access_token</font>
。在发放令牌前,服务器会校验客户端的凭据,有时还需要校验访问用户的身份。授权服务器决定令牌的有效期、权限范围、目标受众等特性。用 Spring Security 和 Spring Authorization Server,可以方便地为现有的 Spring MCP 服务器加上这两大安全能力。
这里以官方例子仓库的【天气】MCP 工具演示如何集成 OAuth2,主要是让服务器端能签发和校验令牌。
首先,<font style="color:rgb(30, 107, 184);">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 style="color:rgb(30, 107, 184);">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 style="color:rgb(30, 107, 184);">/secret</font>
作为固定凭据。 比如 最后一步是开启授权服务器和资源服务器功能。通常会新增一个安全配置类,比如 <font style="color:rgb(30, 107, 184);">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 style="color:rgb(30, 107, 184);">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 的java sdk 没有提供api直接调用, 经过徐庶老师研究源码后, 你只能通过2种方式实现:
扩展mcp 的sse方式java sdk的源码, 整个重写一遍。 工作量较大, 并且我预计过不了多久, spring ai和mcp协议都会更新这块。 看你的紧急程度, 如果考虑整体扩展性维护性,可以整体重写一遍:
提供一个重写思路
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();
}
}
使用:
在用Spring-ai-M8版本的时候, 发现提供了WebClientCustomizer进行扩展。 可以尝试:
curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
@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 进行重新授权
:::
代码:已经上传知识星球
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-05-27
AI在药物研发中的应用:从“试错模式”转向“计算驱动”
2025-05-27
知识体系管理|1个方法教你从搜集到循环运用整套方法流程
2025-05-27
Z Product|前麦肯锡员工创办AI尽职调查公司,专注原始数据收集,赋能企业24小时完成尽调,获数千万美元融资
2025-05-27
开源协作新神器Docmost:团队知识管理的高效低成本解决方案!
2025-05-27
AI问答系统的挑战之语义鸿沟与知识盲点,让大模型理解行业黑话
2025-05-26
大模型落地的 “确定性答案”,藏在知识库里?
2025-05-26
飞书知识问答 | 企业级知识管理进入AI时代
2025-05-26
企业知识管理体系与应用
2024-09-14
2025-01-23
2024-11-07
2024-07-10
2025-02-17
2024-04-24
2024-08-04
2024-06-23
2025-03-09
2024-05-15
2025-05-26
2025-05-14
2025-05-07
2025-05-07
2025-04-27
2025-04-20
2025-04-17
2025-04-17