☰
Spring AI MCP 服务安全实践:用 API Key 认证锁住工具调用入口
2026/9/25 2:35:18 网站建设 项目流程

前段时间帮一个客户评审 AI Agent 项目,他们的 Spring AI 服务已经接了好几个 MCP 工具——查订单、读库存、触发工单。功能没问题,问题出在安全上:这些工具直接挂在公网,没有任何认证,等于把企业核心操作接口原样暴露出去。我当时的建议很简单:在最前面加一把锁,用 Spring Security 做 API Key 认证。这个需求听起来基础,真正落地时还是有不少细节,我把完整过程记录下来。

这篇分享适合正在用 Spring AI 暴露 MCP 工具的后端开发者,或者想把 MCP Server 在团队内部安全共享的架构师。如果你还停留在“内网地址没人知道”这种认知,那更要读下去。我们这里不讨论 MCP 协议本身的语义,只讲清楚一件事:如何用 Spring Security 和 API Key,把一个公开可访问的 Spring AI MCP 服务收回到只有持有密钥的客户端才能调用。

1. MCP 服务的安全边界:先搞清楚要保护什么

1.1 Spring AI 里的 MCP Server 到底暴露了什么

MCP 的全称是 Model Context Protocol,模型上下文协议。它解决了 AI 应用和外部工具、数据源之间的通信标准问题。在 Spring AI 项目中,你只需要在任意 Spring Bean 的方法上打一个@Tool注解,方法就会变成一个 MCP 工具,可以被支持 MCP 协议的客户端发现和调用。

举个例子,下面这段代码几乎是最小可运行的 MCP 工具:

@Component public class OrderTools { @Tool(description = "根据订单号查询订单状态") public String getOrderStatus( @ToolParam(description = "订单号") String orderId) { // 内部逻辑:查数据库、调订单中心、返回状态 return orderService.getStatus(orderId); } }

Spring AI 的 MCP Server 会把OrderTools注册为 MCP 端点,客户端通过 JSON-RPC 调用tools/list可以枚举所有工具,通过tools/call可以执行具体方法。危险就藏在这里:只要 HTTP 端口暴露,任何能访问这个地址的人都可以:

  • 枚举你的工具清单,了解系统内部有哪些业务能力;
  • 调用getOrderStatus查询任意订单号,可能造成越权访问;
  • 如果工具里有写操作,比如发送工单、修改配置、写入文件,后果更严重;
  • 反复调用消耗服务资源,成为自动化攻击的跳板。

很多团队把 MCP 服务和 AI Agent 放在同一个应用里,想的就是“AI 能调工具就行”,却忽略了工具本身就是一组有真实副作用的 API。MCP 客户端通常会自动组合多个工具完成复杂任务,一旦被恶意利用,它不是只调一个接口,而是可以编排一连串操作。

1.2 未认证的后果可能比数据库裸奔更隐蔽

数据库裸奔时,至少数据库端口和连接串还有一层隔离,普通攻击者未必能立刻拿到数据。但 MCP 服务是标准的 HTTP 端点,路径固定,请求方式公开,客户端 SDK 一堆,扫描到 Spring Boot 应用后很容易继续探测/mcp、/api/mcp这类路径。

更隐蔽的是,MCP 工具返回的内容是给 AI 模型消费的结构化文本,攻击者可以直接利用工具返回信息做二次利用。比如某个工具设计为“读取用户上传的文件”,在无认证状态下,任何人都可以遍历文件名;某工具设计为“调用外部大模型生成摘要”,攻击者可以刷你的额度。这些损失往往不是直接扣钱,而是数据泄露、费用消耗、业务被篡改,事后很难追溯。

我在实际项目里遇到过更头疼的情况:MCP 服务没有认证,但是有监听随机端口。团队觉得端口随机就安全了,结果一条“重启后端口变了,客户端要怎么连”的问题暴露了他们所有 MCP 模块都可以被局域网内任意主机直接访问。随机端口只能增加扫描成本,真正该做的是认证和授权。

1.3 为什么路径隐蔽和随机端口不能替代认证

有人喜欢把 MCP 端点放在一个很长的随机路径上,比如/internal/ai/tools/9f2d61e4-28a1-4f2a-927e-8e1e5c5e7f0a,然后告诉我“这样别人猜不到”。猜不到不等于没有风险:Spring Boot 应用通常会暴露/actuator、/error、/swagger-ui等常见路径,攻击者通过这些入口可能获得路由信息;日志系统、API 网关、链路追踪组件都可能记录完整 URL;安全扫描器也会尝试枚举路径字典,长路径顶多增加一点点枚举成本。

认证才是边界。API Key 是成本最低、最容易让客户端接受的一种方案。它不需要用户体系、不需要跳转登录页、不需要证书管理,客户端只需在请求头里带一个固定字符串即可。对于内部团队使用的 MCP 服务,这已经足够。

2. API Key 认证方案设计:Spring Security 为什么是更优解

2.1 API Key 与其他认证方式对比

在设计认证方案之前,我习惯先拉一张对比表,避免脑袋一热就上手写代码。MCP 服务常见的认证方式有三种:API Key、JWT/OAuth2、mTLS。

方案适用场景优点缺点
API Key内部工具服务、B端固定客户端实现简单、调试方便、客户端零依赖密钥需要妥善保管,无法快速识别用户,只能整体轮换
JWT / OAuth2多租户、第三方开发者可表达身份和权限,可配合刷新令牌撤销访问需要 token 端点、密钥签名、权限模型,实现复杂度高
mTLS服务间高安全通信双向证书认证,传输层自带身份证书签发和运维成本高,外部浏览器/工具客户端很难接入

对大多数 Spring AI MCP 服务来说,使用者不是终端用户,而是内部 AI Agent、开发工具、脚本和固定业务服务。API Key 足够解决问题。如果你未来要开放给第三方开发者,再往 OAuth2 方向演进也不迟,Spring Security 的安全过滤模型可以平滑替换。

2.2 请求头约定:X-API-Key 还是 Authorization

API Key 放在哪里是个细节,但直接影响客户端接入体验。最常见的两种做法是:

  • 自定义头:X-API-Key: your-key
  • 标准头:Authorization: Bearer your-key

很多 AI 客户端、MCP 调试工具内置了对Authorization: Bearer的支持,因为大模型 API 普遍用这种格式。但也有不少内部脚本习惯用X-API-Key。为了避免客户端为实现而争吵,我建议服务端同时兼容两种,优先读取X-API-Key,读不到时再看Authorization头里的Bearer前缀。

private String resolveApiKey(HttpServletRequest request) { String xApiKey = request.getHeader("X-API-Key"); if (xApiKey != null && !xApiKey.isBlank()) { return xApiKey.trim(); } String authorization = request.getHeader("Authorization"); if (authorization != null && authorization.startsWith("Bearer ")) { return authorization.substring(7).trim(); } return null; }

注意不要支持?apiKey=xxx这种查询参数方式。查询参数容易出现在访问日志、网关日志、浏览器历史记录里,密钥泄露风险远大于放在 Header 中。

2.3 为什么还是用 Spring Security,而不是手写一个 Servlet Filter

只验证请求头的话,自己写一个OncePerRequestFilter确实很快,十分钟就能完成。但真实项目不会只有认证,后续几乎一定会遇到:CORS 策略、CSRF 防护、异常响应、安全响应头、会话策略、接口限流、审计日志。这些能力 Spring Security 已经帮你封装好了,你只写一个过滤器接入到过滤链里就行。

另一个原因是:MCP 端点不是普通 Spring MVC Controller,@PreAuthorize这类基于 AOP 的方法级安全有时不会拦截 MCP 工具调用。工具调用由 Spring AI 的 MCP dispatcher 接管,不走HandlerInterceptor那套逻辑。所以真正可靠的控制点就是SecurityFilterChain这一层。与其在业务工具类里到处加判断,不如在 HTTP 入口统一认证。

3. Spring Boot 3 配置迁移:从 WebSecurityConfigurerAdapter 到 SecurityFilterChain

3.1 迁移背景

很多 Spring Boot 2.x 时期的项目,安全配置长这样:

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/mcp/**").authenticated(); } }

Spring Security 5.8 开始标记WebSecurityConfigurerAdapter为废弃,Spring Boot 3(对应的 Spring Security 6.x)已经删除这个基类。网上能搜到大量基于 Spring Boot 2 的旧教程,直接复制到 Spring Boot 3 里是编译不过的。

正确的做法是声明一个SecurityFilterChainBean,用 lambda 配置。以 Spring Boot 3.2 以后的标准写法为例:

@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); return http.build(); }

如果你在升级旧项目,建议直接按新的SecurityFilterChain模型重写安全配置,而不是通过兼容配置保留旧类。保留旧类虽然能编译,但后续升级 Spring Security 7 时还会再踩一遍坑。

3.2 默认行为带来的坑

只引入spring-boot-starter-security,不写任何安全配置时,Spring Boot 会有一个默认的过滤器链:表单登录、HTTP Basic、随机生成的 user 密码。也就是说,你刚把 MCP 服务部署好,/mcp端点会返回重定向到登录页,而不是 401 JSON,客户端根本没法接。

这个默认行为很容易误导调试。你会看到 MCP 客户端报“连接失败”或“收到 HTML 响应”,而不是明确的 401。排查时要先确认自己的SecurityFilterChain是否生效,再看端点的实际响应头。

3.3 SecurityFilterChain 的多链路匹配

Spring Security 6 支持定义多个SecurityFilterChain的 Bean,按@Order排序,第一个匹配请求的就生效。这很适合我们的场景:/mcp/**走 API Key 认证链路,其他路径可以放行或走另一套规则。

@Bean @Order(1) SecurityFilterChain mcpSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher("/mcp/**") // 接下来配置 API Key 认证 ; return http.build(); } @Bean @Order(2) SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); return http.build(); }

securityMatcher是链路的入口。只有当请求路径匹配/mcp/**时,才会进入 API Key 认证逻辑。这样不会影响应用的静态资源、健康检查、普通接口。

4. 完整落地:用 API Key 锁住 Spring AI MCP Server

4.1 依赖与版本说明

以 Spring Boot 3.3 / Spring AI 1.0.0 这个组合为例,Maven 依赖如下:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>

注意 Spring AI 版本更新很快,1.0.0-M预览版和2.0.0的某些自动配置项可能不同。我在项目中通常把 Spring AI 版本提到是1.0.0正式版或更高,然后根据启动日志确认 MCP 端点路径。多数情况下默认端点是/mcp,但你还是要在测试环境里实测确认。

4.2 定义 MCP 工具

为了演示,我写一个很简单的 MCP 工具,模拟查询告警信息:

@Component public class AlertTools { @Tool(description = "根据告警 ID 查询告警详情") public String getAlertDetail( @ToolParam(description = "告警 ID") String alertId) { return "告警 " + alertId + " 状态: PENDING, 级别: P1"; } }

启动应用后,Spring AI 自动把这个工具注册到 MCP Server。如果不用任何安全配置,直接 POST 到/mcp就能看到工具列表。

4.3 设计密钥校验器

密钥校验不一定要走完整的AuthenticationProvider,因为内部静态密钥比对逻辑很直白。我更建议轻量封装一个ApiKeyVerifier,把密钥来源统一管理起来。

@Component public class ApiKeyVerifier { private final Set<String> validKeys; public ApiKeyVerifier(@Value("${mcp-security.api-keys:}") String rawKeys) { this.validKeys = Arrays.stream(rawKeys.split(",")) .map(String::trim) .filter(key -> !key.isEmpty()) .collect(Collectors.toSet()); } public boolean isValid(String apiKey) { return validKeys.contains(apiKey); } }

对应的application.yml:

mcp-security: api-keys: local-key-001,team-a-key-002

生产环境请不要把密钥直接写在 YAML 里,用环境变量覆盖:

mcp-security: api-keys: ${MCP_SECURITY_API_KEYS}

环境变量里用逗号分隔多个 key,比如MCP_SECURITY_API_KEYS=key1,key2。

4.4 自定义 API Key 认证过滤器

这个OncePerRequestFilter的任务是:从 Header 中解析出 API Key,如果校验通过,就把认证信息写入SecurityContext。

public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { private final ApiKeyVerifier apiKeyVerifier; public ApiKeyAuthenticationFilter(ApiKeyVerifier apiKeyVerifier) { this.apiKeyVerifier = apiKeyVerifier; } @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String apiKey = resolveApiKey(request); if (apiKey != null && apiKeyVerifier.isValid(apiKey)) { List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_MCP")); ApiKeyAuthenticationToken authentication = new ApiKeyAuthenticationToken(apiKey, authorities); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); SecurityContextHolder.setContext(context); } try { filterChain.doFilter(request, response); } finally { SecurityContextHolder.clearContext(); } } }

这里的关键点是:如果 key 不存在,或者 key 校验失败,我们什么都不做,只是放行。后续由ExceptionTranslationFilter发现没有认证,从而返回 401。如果错误地直接返回 403,客户端会有点懵,因为明明是没带密钥而不是权限不足。

ApiKeyAuthenticationToken是一个自定义 Token,继承AbstractAuthenticationToken:

public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { private final String apiKey; public ApiKeyAuthenticationToken(String apiKey, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.apiKey = apiKey; setAuthenticated(true); } @Override public Object getCredentials() { return apiKey; } @Override public Object getPrincipal() { return "mcp-client"; } }

也许你会问,为什么不直接在 Filter 里校验失败时调用response.sendError(401),还要走后续链?我的经验是,统一交给AuthenticationEntryPoint处理,更容易保持 JSON 响应格式一致,以后想增加审计日志或者跳转逻辑,只需要改一个入口。

4.5 SecurityConfig 完整配置

现在把这些组件串起来:

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean @Order(1) SecurityFilterChain mcpSecurityFilterChain(HttpSecurity http, ApiKeyVerifier apiKeyVerifier) throws Exception { ApiKeyAuthenticationFilter filter = new ApiKeyAuthenticationFilter(apiKeyVerifier); http .securityMatcher("/mcp/**") .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .anyRequest().hasRole("MCP")) .exceptionHandling(ex -> ex .authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write( "{\"code\":\"api_key_required\",\"message\":\"API key is required in X-API-Key or Authorization Bearer header\"}"); }) .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write( "{\"code\":\"forbidden\",\"message\":\"invalid api key or insufficient permission\"}"); })) .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean @Order(2) SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); return http.build(); } }

我特意在配置里加了/actuator/health放行。很多 MCP 客户端和运维系统会先探活,如果连健康检查都被 401 挡住,会误判服务不可用。你也可以根据自己的运维需求调整白名单,但原则是:尽量只放开“不需要认证且没有业务数据暴露”的端点。

hasRole("MCP")等价于要求ROLE_MCP权限。我们给通过 API Key 校验的请求注入了这个角色,所以只有携带正确 key 的请求能放行。未携带 key、key 错误、key 过期,都会进入AuthenticationEntryPoint,返回 401。

5. 启动验证与请求测试

5.1 不带 Key 的请求

启动应用后,先用 curl 测一下完全不带头的情况:

curl -i -X POST http://localhost:8080/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

预期响应是 401:

HTTP/1.1 401 Content-Type: application/json;charset=UTF-8 {"code":"api_key_required","message":"API key is required in X-API-Key or Authorization Bearer header"}

这一步主要验证SecurityFilterChain的匹配路径和AuthenticationEntryPoint是否生效。如果返回 200,十有八九是securityMatcher的路径与 Spring AI 实际暴露的 MCP 路径不一致。

5.2 带错误 Key 的请求

curl -i -X POST http://localhost:8080/mcp \ -H 'Content-Type: application/json' \ -H 'X-API-Key: wrong-key' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

预期响应同样是 401,因为在 Filter 阶段错误 key 没有获得认证,后续链路把它当作匿名请求处理。这里就不应该出现 200 或 403。如果返回 200,说明 Filter 没有实例化,或者ApiKeyVerifier配置的 key 列表为空。

5.3 带正确 Key 的请求

分别测试两种 Header 写法:

curl -i -X POST http://localhost:8080/mcp \ -H 'Content-Type: application/json' \ -H 'X-API-Key: local-key-001' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
curl -i -X POST http://localhost:8080/mcp \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer local-key-001' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

两种方式都应返回 200,JSON-RPC 响应体里能看到 Spring AI 注册的 MCP 工具列表,比如:

{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "getAlertDetail", "description": "根据告警 ID 查询告警详情", "inputSchema": {...} } ] } }

5.4 常见 401/403 排查

我在实际联调中遇到过几类典型问题。

第一,请求返回的是 Spring Security 默认的 403 HTML 页面而不是我们自定义的 401 JSON。原因是没写exceptionHandling,或者securityMatcher没匹配上,请求落到了默认链路上。检查Order(1)的securityMatcher是否精确,以及defaultSecurityFilterChain是不是把/mcp也放行了。

第二,MCP 客户端报unexpected status 401 unauthorized: incorrect api key provided。这类提示在浏览器和大模型客户端工具里很常见,通常是客户端发送的 Header 名不对。例如它默认发的是Authorization: Bearer,而你的 Filter 只读了X-API-Key。兼容两种 Header 能直接避免这个问题。

第三,带 key 正确但返回 403。多半是认证 Token 没有拿到ROLE_MCP角色。检查自定义 Token 构造时是否传了SimpleGrantedAuthority("ROLE_MCP"),并且hasRole("MCP")是否刚好匹配。少传角色,认证通过了但授权失败,于是返回 403。

第四,tools/call调用时出现 401 但tools/list正常。这种场景不常见,因为同一个链路应该统一生效。如果出现,先检查路径是不是有上下文前缀,比如网关转发/api/mcp,内层应用可能只匹配了/mcp/**。安全匹配要以网关转发后到达应用的最终路径为准。

6. 生产环境需要注意的细节

6.1 密钥管理与轮换

API Key 没有内置过期机制,所以轮换要靠流程。我的习惯是至少维持两个有效 key:一个给旧客户端过渡,一个给新客户端切换。等所有客户端都切到新 key 后,再把旧 key 从环境变量里移除并重启应用。

如果你在日志中看到错误 key 请求,别急着调代码。先记录 key 前缀和来源 IP,判断是不是扫描器或者内部脚本写错了 key。密钥轮换后,旧 key 不要立即删除,至少要留一个观察周期,避免客户端缓存导致眼巴巴看着别人正常而你这边服务不可用。

密钥本身建议用随机生成的字符串,长度至少 32 位,不要用springai123这种。官方一点的说法是使用加密安全的随机数生成器,实际做法是本地执行openssl rand -hex 32,把输出复制到环境变量里。

6.2 多客户端、多角色策略

如果只有几个内部团队,一个 key 列表就够了。但团队多了以后,你需要知道“谁在调”,否则没法审计。一个简单的演进方案是维护 key 到客户端名的映射,校验时读取客户端名称放入 Token 的principal,这样后续可以按客户端维度限流、计数、审计。

public class ApiKeyVerifier { private final Map<String, String> keyToClientName = new HashMap<>(); public ApiKeyVerifier(@Value("${mcp-security.api-keys:}") String rawKeys) { for (String entry : rawKeys.split(",")) { if (entry.isBlank()) continue; String[] parts = entry.split(":"); if (parts.length == 2) { keyToClientName.put(parts[0].trim(), parts[1].trim()); } } } public Optional<String> getClientName(String apiKey) { return Optional.ofNullable(keyToClientName.get(apiKey)); } }

对应配置:

mcp-security: api-keys: ${MCP_SECURITY_API_KEYS}

环境变量里每个 key 用key:clientName表示,比如key1:order-agent,key2:data-sync。这样同一个 MCP 服务既能识别客户端身份,又不需要立刻引入 OAuth2。

6.3 与 Spring Cloud Gateway 统一鉴权

如果你的系统已经用了 Spring Cloud Gateway,并且有多个 MCP Server 或多个业务服务,我不建议在每个服务里各写一套 API Key 校验。更好的方式是在网关层统一拦截,校验通过后再将请求转发到下游。

网关层可以做两件事:一是在GlobalFilter里解析并校验 API Key;二是校验通过后把X-Client-Name等头写入转发的请求,下游服务直接信任网关即可。这样 MCP 服务本身可以构建在相对可信的内网环境,网关成为唯一的外部认证入口。

这个设计对团队的意义不只是少写代码,更重要的是统一了安全策略:网关下线某个 key、限流、审计,所有下游服务同时生效。如果你还在用 Spring Cloud 和 Spring AI 开发 Agent,非常推荐把安全防护做在入口网关,而不是分散到每个工具服务里。

6.4 版本兼容性:Spring Security 7 与 Spring AI 2.0

热词里既有 Spring Security 7 也有 Spring AI 2.0,说明社区已经开始关注下一代版本。Spring Security 7 的路线图我关注过,整体会延续SecurityFilterChain模型,API Key 这类自定义 Filter 的接入方式不会发生根本性变化。主要风险不在安全配置,而在于 Spring Boot 3.x 到 4.x 的自动配置变化。

Spring AI 2.0 对 MCP 的@Tool注解和端点注册可能继续演进,但安全边界的设计原则不会变:MCP 端点不该裸奔。哪怕版本升级后默认端点路径变了,只要你的securityMatcher跟着新配置调整,认证逻辑基本可以迁移。

升级版本时,我建议先做一次端到端测试,用正确 key 和错误 key 分别验证。不要只看“服务起来了”,要确认/mcp的响应码和响应体符合预期。因为 Spring AI 自动配置变更时,最容易被忽略的就是 MCP 端点路径和 Content-Type 处理。

经验一则

这轮改造做完之后,我在项目里又做了一次内网扫描,确认没有遗漏的 MCP 端点。后来查访问日志时发现,每天都有不少无 key 请求打进来,说明这把锁确实加对了。就我个人的习惯,给 MCP 服务加 API Key 不是一道加分题,而是上线前必须完成的一步。你可以在本地先按文章里的最小代码跑通,再逐步加上自己的密钥管理、审计和网关策略。希望这篇分享能让你少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询