SpringAI SSE MCP Server 安全加固:API Key 鉴权配置与验证
1. 为什么你的 SpringAI SSE MCP Server 需要 API Key 鉴权SpringAI 的 SSE MCP Server 本质上是把工具调用能力通过 Server-Sent Events 长连接暴露出去客户端连上/sse端点后就能拿到 session再通过/mcp/message发指令。问题在于SpringAI 1.0.0 默认没有给这条链路加任何身份校验只要知道地址和端口谁都能连上来调用你注册的 Tool。本地开发无所谓一旦你把服务放到内网共享、或者用 frp/nginx 映射到公网做联调这个端点就等于裸奔。我见过最常见的翻车场景是开发者把 MCP Server 跑在测试机上同事随手 curl 一下就能列出所有工具、甚至触发写文件、查数据库这类高危操作。所以这篇不讲虚的直接给一套可复制的 API Key 鉴权骨架把无鉴权的 SSE 端点收敛成只有带对 Key 的请求才能通。适合正在用 SpringBoot 3.x SpringAI 1.0.0 搭 MCP Server、并且准备把服务暴露到本机以外的人。核心思路很简单用 Spring MVC 的HandlerInterceptor在请求进入 Controller 之前拦一道校验Authorization: Bearer key头不通过就直接 401。下面从环境到验证一步步来。2. 前置准备环境版本与 TaoToken 接入先把版本对齐SpringAI 的 MCP 支持在不同小版本间 API 有差异混用容易出玄学问题。我实测稳定的组合是组件版本说明JDK17SpringBoot 3.x 最低要求SpringBoot3.5.0与 SpringAI 1.0.0 兼容SpringAI1.0.0含 MCP Server SSE 支持如果你在本地调试时想让 MCP Server 背后的模型调用走统一入口可以先把模型侧的 Key 准备好。TaoToken 的 API 地址是https://taotoken.net/api控制台在 consoleKey 在 API Keys 页面生成。想先验证模型通不通用 模型对话 试一句就行如果是长期跑编码类 Agent可以看 Coding Plan。注意MCP Server 自身的 API Key 和模型侧的 Key 是两回事。前者是你自己发给调用方的凭证后者是访问模型服务的凭证别混在一个配置项里。3. 可复制的鉴权配置骨架3.1 拦截器校验 Bearer Token新建ApiKeyValidateInterceptor.java。这里我把类名从原文的ApkKeyValidateInterceptor改成ApiKeyValidateInterceptor拼写更规范也方便你搜索。package com.example.mcp.server.config; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerInterceptor; import java.util.List; import java.util.Objects; public class ApiKeyValidateInterceptor implements HandlerInterceptor { private static final String BEARER Bearer; private final ListString apiKeys; public ApiKeyValidateInterceptor(String[] apiKeys) { this.apiKeys List.of(apiKeys); } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (checkApiKey(request)) { return true; } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\invalid api key\}); response.flushBuffer(); return false; } private boolean checkApiKey(HttpServletRequest request) { String authHeader request.getHeader(Authorization); if (authHeader null || authHeader.isBlank()) { return false; } String[] parts authHeader.split( ); if (parts.length ! 2) { return false; } if (!Objects.equals(parts[0], BEARER)) { return false; } return apiKeys.contains(parts[1]); } }几个关键点值得说清楚。第一preHandle返回false时请求会被直接截断不会进 Controller所以 SSE 连接根本建立不起来这是我们要的效果。第二返回体我改成了 JSON方便前端和 curl 统一解析原文只写了纯文本。第三apiKeys用List存contains判断支持多 Key 并存。3.2 注册拦截器并锁定路径新建WebConfig.java把拦截器挂到/sse/**上package com.example.mcp.server.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { Value(${mcp.apiKeys}) private String[] apiKeys; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ApiKeyValidateInterceptor(apiKeys)) .addPathPatterns(/sse/**, /mcp/**); } }这里比原文多拦了/mcp/**。原因很实际SSE 建连走/sse但后续发消息走的是/mcp/message如果只拦/sse攻击者拿到 sessionId 后仍可能绕过。两个路径一起拦才闭环。3.3 配置文件与 Key 轮换在application.properties里配置mcp.apiKeysgwZOomw8cjubdv4Nbm5w9uZTpsF3MLSq,lnWaaR6IQMToeFmbygxWM1E0NiK1YnWo逗号分隔支持多 Key这带来一个实用能力平滑轮换。你可以先把新 Key 追加进列表通知调用方切换等旧 Key 的调用量归零后再删掉全程不用重启服务。生产环境建议把 Key 放到环境变量或配置中心别硬编码进仓库。4. 三种请求的验证动作与预期结果服务起来后用 curl 分别验证三种情况。假设端口 8080。未带 Keycurl -i http://localhost:8080/sse预期返回HTTP/1.1 401body 是{code:401,msg:invalid api key}连接立即关闭不会挂起。错误 Keycurl -i -H Authorization: Bearer wrong-key-123 http://localhost:8080/sse同样 401。注意这里 Key 格式对但值不对走的是contains判断失败分支。正确 Keycurl -i -N -H Authorization: Bearer gwZOomw8cjubdv4Nbm5w9uZTpsF3MLSq \ -H Accept: text/event-stream \ http://localhost:8080/sse预期返回HTTP/1.1 200并且你会看到 SSE 流开始推送类似event: endpoint data: /mcp/message?sessionIdxxxx-xxxx拿到 sessionId 后后续发消息也要带上同一个 Authorization 头否则/mcp/**拦截器会拦掉。这一步是很多人漏掉的建连成功不代表后续调用成功两个端点都要带 Key。5. 本篇常见错误排查401 一直返回但 Key 明明是对的先确认请求头名字大小写。HTTP 头本身不区分大小写但如果你用了某些自定义网关做了头改写可能把Authorization吞掉了。用curl -v看实际发出的头。SSE 建连成功但发消息 401检查addPathPatterns是否包含/mcp/**。只配/sse/**时/mcp/message是放行的但如果你反过来只配了/mcp/**建连就会失败。拦截器不生效确认WebConfig上有Configuration且被 Spring 扫描到。如果 MCP Server 用的是 WebFlux 而不是 WebMVCHandlerInterceptor不适用需要改用WebFilter。SpringAI 的 SSE MCP Server 默认基于 WebMVC但如果你手动引入了 WebFlux 依赖行为会变。多 Key 配置注入失败Value(${mcp.apiKeys})注入String[]时Spring 会按逗号自动切分。如果配置里带了空格比如key1, key2第二个 Key 前面会多一个空格导致匹配失败。写成key1,key2不要留空格。Key 泄露风险拦截器里不要打印完整 Key 到日志。调试时最多打印前 4 位加****否则日志系统本身就是泄露点。6. 把鉴权接进你的调用链到这里你的 SSE 端点已经从「谁都能连」变成「带对 Key 才能连」。下一步是把调用方也改造成带 Key 的形态如果是 Java 客户端在WebClient或RestTemplate上统一加Authorization头如果是 Claude Code 这类 Agent 通过 MCP 接入需要在 MCP 配置里补上请求头字段具体格式可以对照 接入文档 里的 MCP 章节Claude Code 的配置示例在 ClaudeCodeAnthropic 页面。一个我踩过的坑轮换 Key 时忘了同步更新 Agent 侧的配置结果建连一直 401排查了半天以为是拦截器逻辑写错了。后来养成习惯改 Key 先在测试环境用 curl 跑一遍三种情况确认无误再推生产。