MCP最新企业级权限认证方案,STDIO/SSE不同实现详解!
在企业级MCP方案落地时,权限控制是个绕不开的话题。总不能谁都能访问MCP Server吧?或者更实际一点,不同用户进来,返回的数据也得不一样。这就涉及MCP Server的授权操作了。

而MCP Server有两种传输方式,实现起来的路子也不太一样。
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端,用System.getenv()就能拿到env里的变量:
@Tool(description = "获取用户余额")
public String getScore() {
String userName = System.getenv("API_KEY");
// todo .. 鉴权处理
return "未检索到当前用户"+userName;
}
当然,也可以通过AOP的方式统一处理,更优雅一些。
不过,这里必须敲黑板:这种方式不支持动态鉴权。你不能指望在运行中随意更换环境变量。因为STDIO是本地运行,MCP Server是作为子进程启动的。如果有多个用户要动态切换凭证,大家就会一起抢共享的环境变量,最后只能存一个。除非你给每个用户单独开一个STDIO MCP Server,但这显然不现实,性能根本扛不住。所以,如果要搞多用户动态切换授权,还是得看SSE。
SSE
说明
如果你想把MCP服务器开放给外部使用,那就要暴露标准的HTTP接口。在私有场景下,可能不需要太严格的身份认证,但在企业级部署中,安全权限就变得至关重要了。2025年3月发布的最新MCP规范,干脆引入了安全基础,直接用上了广泛使用的OAuth2框架。
OAuth2的具体细节这里就不展开了,但简单回顾一下很有必要。
在规范的草案中,MCP服务器既是资源服务器,也是授权服务器。
- ,MCP负责检查每个请求中的
作为资源服务器
Authorization请求头。这个请求头里必须包含OAuth2的access_token(令牌),它相当于客户端的"通行证"。这个令牌通常是JWT(JSON Web Token),也可能是一个不可读的随机字符串。如果令牌缺失或无效(比如无法解析、已过期、不是发给本服务器的等等),请求会被直接拒绝。正常的调用示例如下:
curl https://mcp.example.com/sse -H "Authorization: Bearer <有效的 access token>"
- ,MCP还需要有能力安全地为客户端签发
作为授权服务器
access_token。在发放令牌前,它会校验客户端的凭据,有时还要校验访问用户的身份。授权服务器决定令牌的有效期、权限范围、目标受众等特性。
借助Spring Security和Spring Authorization Server,可以很方便地给现有的Spring MCP服务器加上这两大安全能力。
给Spring MCP服务器加上OAuth2支持
我们用官方例子仓库里的"天气"MCP工具来演示。核心目标就是让服务器端既能签发令牌,又能校验令牌。
首先,在pom.xml里加上必要的依赖:
org.springframework.boot
spring-boot-starter-oauth2-resource-server
org.springframework.boot
spring-boot-starter-oauth2-authorization-server
接着,在application.properties里配置一个简易的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请求跟授权服务器交互了,不需要浏览器,用配置好的/secret作为固定凭据。最后一步是开启授权服务器和资源服务器的功能。通常要新增一个安全配置类,比如SecurityConfiguration:
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。可以通过client_credentials授权方式(适用于机器到机器、服务账号的场景):
curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
# 返回:
# {"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的Ja va SDK还没有提供直接的API来调用。研究过源码后,发现只能通过两种方式实现:
重写源码
一种办法是把MCP的SSE方式的Ja va 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 connections = new HashMap();
private final Map headersMap = new HashMap<>();
private String defaultHeaderName;
private String defaultHeaderValue;
private boolean enableCompression = false;
private int connectionTimeout = 5000;
// ... 省略 getter/setter ...
}
重写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"},
ha vingValue = "true",
matchIfMissing = true
)
public class SseWebFluxTransportAutoConfiguration {
// ... 核心逻辑,在构建WebClient时注入headersMap ...
}
设置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() {
return (builder) -> {
builder.defaultHeader("Authorization","Bearer eyJraWQiOiIzYmMzMDRmZC02NzcyLTRkYTItODJiMy1hNTEwNGExMDBjNTYiLCJhbGciOiJSUzI1NiJ9...");
};
}
这里需要特别说明一下:SSE是支持动态切换token的,因为每个请求都是一个全新的HTTP请求,不会出现多线程争抢的情况。如果需要动态授权,每次可以重新执行 curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666 来获取新的令牌。