ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MCP学习:手把手教你扩展SpringAI MCP Server源码,轻松实现认证鉴权!

MCP学习:手把手教你扩展SpringAI MCP Server源码,轻松实现认证鉴权! 1. 为什么 SpringAI MCP Server 默认裸奔鉴权到底卡在哪MCP 这两年火得很快很多团队已经把内部工具、数据库查询、运维脚本包装成 MCP Server 给大模型调用。但只要你把 STDIO 模式换成 HTTP SSE 模式对外提供服务问题立刻暴露默认情况下SpringAI 的 MCP Server 没有任何身份校验谁拿到/sse地址谁就能连连上就能调你注册的所有工具。这在本地玩没问题一旦部署到测试环境或者内网共享等于把工具箱钥匙插在门上。我见过最常见的三种“补救”写法都不太理想。第一种是在每个 Tool 方法里手动读HttpServletRequest拿 header代码重复且侵入业务第二种是加一个 SpringHandlerInterceptor但 MCP 的 SSE 连接是长连接/sse建立会话和/messages传消息是两个不同阶段拦截器拦得住第一个不一定拦得住第二个第三种是直接上 OAuth功能是完整但对一个内部工具来说引入授权服务器、客户端注册、token 刷新这一整套学习成本和维护成本都偏高。所以真正要解决的问题是在 SpringAI MCP Server 的传输层扩展一个轻量鉴权点用 API-Key / Bearer 这种 HTTP 头或请求参数的方式完成校验并且让校验结果能跨线程传递到业务方法里。这需要理解 SpringAI 的自动装配链路McpWebMvcServerAutoConfiguration负责构造传输层 ProviderMcpServerAutoConfiguration依赖这个 Provider 给 MCP Server 提供能力。我们要做的就是复制并重写WebMvcSseServerTransportProvider在它构造的两个路由函数handleSseConnection和handleMessage里插入鉴权逻辑。这里有个容易踩的坑MCP 框架处理请求的线程和实际执行 Tool 的线程不是同一个用RequestContextHolder在业务方法里取请求信息会拿到 null。解决办法是用阿里 TTLTransmittableThreadLocal做跨线程数据传递。下面我会把完整改造步骤、可复制的配置片段、以及如何把 endpoint 指向 TaoToken 统一 Key 通道联调一步步写清楚。适合已经能跑通基础 SpringAI MCP Server、想加一层鉴权的后端同学。2. 前置准备TaoToken 统一 Key 通道与 MCP 依赖梳理在动手改源码之前先把两件事准备好一是 MCP Server 工程本身能跑二是想清楚鉴权 Key 从哪来、怎么统一管理。很多同学卡在“我本地校验通过了但换台机器 Key 就乱”本质是没有一个统一的 Key 分发和调用通道。我现在的做法是把模型调用和 MCP 工具调用的出口都收敛到 TaoToken 的统一 API 通道。它的 API 地址是https://taotoken.net/api控制台在https://taotoken.net/consoleAPI Keys 管理页在https://taotoken.net/api-keys。你可以在控制台里创建不同用途的 Key比如一个给 MCP Server 做服务端校验一个给客户端调用模型用。这样做的好处是MCP Server 的鉴权逻辑校验的是“这个 Key 是否合法、是否在有效期内”而 Key 的签发和吊销都在统一后台完成不用自己再搭一套 Key 管理系统。前置依赖方面SpringAI MCP Server 的 WebMVC SSE 模式需要这几个核心依赖版本按你工程实际来这里给的是常见组合dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-mcp-server-webmvc-spring-boot-starter/artifactId /dependency dependency groupIdcom.alibaba/groupId artifactIdtransmittable-thread-local/artifactId version2.14.3/version /dependencyTTL 这个依赖是必须的原因前面说了跨线程传递鉴权上下文。如果你不加在 Tool 方法里想拿当前请求的 API-Key 就只能靠方法参数一层层传非常别扭。然后是配置文件。MCP Server 的 SSE endpoint 和 message endpoint 默认是/sse和/mcp/messages建议显式写出来方便后面路由匹配spring: ai: mcp: server: name: auth-mcp-server version: 1.0.0 protocol: SSE sse-endpoint: /sse sse-message-endpoint: /mcp/messages这里要提醒一点sse-message-endpoint的路径必须和客户端配置里的url拼接后能对上。客户端连的是/sse服务端返回的 message endpoint 是相对路径客户端会拿这个相对路径去发 POST。如果你改了路径但客户端没同步会出现连上 SSE 但消息发不出去的情况报错通常是 404 或者reading choices之类的解析失败。关于 Key 的格式建议统一成Authorization: Bearer token这种标准形式同时兼容请求参数?aktoken。这样客户端无论是用 header 还是 query 都能接入百度地图那类 MCP Server 就是 query 参数模式。TaoToken 的 Key 在控制台创建后复制出来即可注意不要提交到 Git用环境变量注入。3. 可复制配置重写 TransportProvider 与鉴权拦截器这一节是核心直接给可复制的代码结构。整体思路复制WebMvcSseServerTransportProvider源码重命名为SupportAuthMcpServerTransportProvider在构造路由时插入鉴权拦截器并用 TTL 保存鉴权上下文。第一步定义鉴权上下文和拦截器接口public class McpAuthRequestContext { private static final TransmittableThreadLocalString API_KEY_HOLDER new TransmittableThreadLocal(); public static void setApiKey(String apiKey) { API_KEY_HOLDER.set(apiKey); } public static String getApiKey() { return API_KEY_HOLDER.get(); } public static void clear() { API_KEY_HOLDER.remove(); } } public interface McpAuthInterceptor { boolean preHandle(String apiKey); }第二步在SupportAuthMcpServerTransportProvider的构造函数里接收拦截器并改造两个路由方法。关键点是在handleSseConnection和handleMessage里先取 Key、再校验、校验通过后写入 TTLpublic class SupportAuthMcpServerTransportProvider extends WebMvcSseServerTransportProvider { private final McpAuthInterceptor authInterceptor; public SupportAuthMcpServerTransportProvider(ObjectMapper objectMapper, String sseEndpoint, String messageEndpoint, McpAuthInterceptor authInterceptor) { super(objectMapper, sseEndpoint, messageEndpoint); this.authInterceptor authInterceptor; } Override protected ServerResponse handleSseConnection(ServerRequest request) { String apiKey resolveApiKey(request); if (!authInterceptor.preHandle(apiKey)) { return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); } McpAuthRequestContext.setApiKey(apiKey); return super.handleSseConnection(request); } Override protected ServerResponse handleMessage(ServerRequest request) { String apiKey resolveApiKey(request); if (!authInterceptor.preHandle(apiKey)) { return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); } McpAuthRequestContext.setApiKey(apiKey); return super.handleMessage(request); } private String resolveApiKey(ServerRequest request) { String header request.headers().firstHeader(Authorization); if (header ! null header.startsWith(Bearer )) { return header.substring(Bearer .length()).trim(); } return request.param(ak).orElse(null); } }第三步在启动类里覆盖默认的 Provider BeanBean public WebMvcSseServerTransportProvider mcpTransportProvider(ObjectMapper objectMapper, McpServerProperties properties) { return new SupportAuthMcpServerTransportProvider( objectMapper, properties.getSseEndpoint(), properties.getSseMessageEndpoint(), apiKey - { if (apiKey null || apiKey.isEmpty()) { return false; } // 这里可以查 Redis 或数据库校验 Key 是否合法、是否过期 return apiKey.startsWith(sk-); }); }第四步客户端配置。把 MCP 客户端的 endpoint 指向你的服务同时带上 Key。如果你希望模型调用也走统一通道把模型 base-url 指向 TaoToken{ mcpServers: { auth-mcp-server: { url: http://localhost:8080/sse, headers: { Authorization: Bearer sk-你的TaoTokenKey } } } }如果你用的是 Claude Code 这类工具配置在settings.json里Base URL 填https://taotoken.net/apiKey 填控制台创建的 KeyModel ID 按你实际使用的模型填。这三件套Base URL Key Model ID缺一不可少一个就会出现 401 或者模型找不到的错误。4. 验证请求从 401 到成功调用工具的完整闭环配置写完后必须做两步验证先验证鉴权拦截生效再验证带正确 Key 能跑通工具调用。第一步用 curl 模拟不带 Key 的请求预期返回 401curl -i -N -H Accept: text/event-stream http://localhost:8080/sse如果返回HTTP/1.1 401 Unauthorized说明拦截器生效了。如果返回 200 并且开始推 SSE 事件说明你的 Provider 覆盖没生效检查启动类里的 Bean 是否真的替换了默认的。第二步带上正确的 Key 再请求curl -i -N -H Accept: text/event-stream \ -H Authorization: Bearer sk-你的TaoTokenKey \ http://localhost:8080/sse预期看到event: endpoint事件data 里是 message endpoint 的相对路径类似/mcp/messages?sessionIdxxx。这一步成功说明 SSE 连接建立且鉴权通过。第三步用 MCP 客户端实际调用一个工具。假设你注册了一个getWeather工具客户端发起tools/call请求。服务端在handleMessage里会再次校验 Key通过后执行工具。你可以在 Tool 方法里通过McpAuthRequestContext.getApiKey()拿到当前请求的 Key用于日志或二次权限判断Tool(description 查询天气) public String getWeather(String city) { String apiKey McpAuthRequestContext.getApiKey(); log.info(当前调用 Key: {}, apiKey); return 晴25度; }实测下来最容易出问题的是 message 阶段的鉴权。因为/sse连接建立时带了 Key但客户端后续 POST 到/mcp/messages时有些客户端不会自动带上 header。这时候要么在客户端配置里显式声明 headers要么服务端支持从 sessionId 关联的会话里取 Key。我建议两者都做连接时把 Key 和 sessionId 绑定存到内存或 Redismessage 阶段优先从 header 取取不到再从会话里取。如果你把模型调用也切到 TaoToken可以在客户端配置里把模型 base-url 设为https://taotoken.net/api这样模型请求和 MCP 工具请求走同一个 Key 体系排查问题时只需要看一个 Key 的状态。验证模型通道是否通可以直接用模型对话页面发一条消息测试确认 Key 有效后再接入 MCP 联调。5. 常见报错排查401、local proxy failed 与 reading choices改造过程中会碰到几类典型报错这里按真实错误信息对照排查。401 Unauthorized最常见。分三种情况。一是 Key 没传检查客户端 headers 配置是否写对注意Authorization大小写和Bearer后面有个空格。二是 Key 传了但校验逻辑返回 false检查你的preHandle实现比如startsWith(sk-)是否和实际 Key 前缀匹配。三是 message 阶段没带 KeySSE 连上了但 POST 消息被拒这时候看服务端日志handleMessage里打印的 apiKey 是否为 null。local proxy failed这个报错通常出现在客户端侧表示客户端无法连接到配置的 endpoint。检查三件事服务端是否真的在监听对应端口/sse路径是否和配置一致如果用了 TaoToken 的 API 地址确认网络能通https://taotoken.net/api。注意不要在任何配置里写代理相关的设置直接连即可。reading choices 解析失败这个报错一般出现在模型调用返回体解析阶段说明返回的不是预期的 JSON 结构。常见原因是 Base URL 配错比如把https://taotoken.net/api写成了带多余路径的地址或者 Model ID 填了一个不存在的模型。检查三件套Base URL 是否为https://taotoken.net/apiKey 是否有效Model ID 是否和控制台里的一致。三个都对还报错用模型对话页面单独测一下 Key。OAuth 相关报错如果你没启用 OAuth 但看到 OAuth 字样说明客户端默认走了 OAuth 流程。在客户端配置里显式指定用 header 鉴权不要让它自动探测。SpringAI 的 MCP 客户端如果检测到 401 且没有配置 header可能会尝试 OAuth 发现流程这时候要么补上 header要么在客户端关掉自动 OAuth。连接建立但工具列表为空SSE 连上了但tools/list返回空。检查你的 Tool 注册类是否被 Spring 扫描到Tool方法所在的 Bean 是否注入到了 MCP Server 的 ToolCallbackProvider 里。这个和鉴权无关但经常和鉴权改造一起出现因为改完 Provider 后容易漏掉 Tool 注册的配置。排查时建议打开 Spring 的 debug 日志把org.springframework.ai.mcp包设为 DEBUG能看到路由匹配和消息流转的详细过程。另外在拦截器里加日志把每次请求的路径、header、参数都打出来比猜快得多。6. 把 endpoint 收敛到 TaoToken统一 Key 与后续扩展鉴权跑通后下一步是把 endpoint 和 Key 管理收敛。我现在的做法是MCP Server 本身不维护 Key 的签发只做校验Key 的创建、吊销、额度查看都在 TaoToken 控制台完成。服务端校验时可以调 TaoToken 的接口确认 Key 状态也可以本地缓存校验结果减少请求。具体接入方式在preHandle里把拿到的 Key 拿去校验。如果你们内部有自己的 Key 服务就查自己的如果想用统一通道就把模型调用和工具调用的出口都指向https://taotoken.net/api。客户端配置里MCP Server 的 url 指向你的服务模型 base-url 指向 TaoToken两边的 Key 用同一个这样权限体系统一排查也方便。后续可以继续优化的点我列几个实际做过的一是支持标准 Bearer 解析把Authorization: Bearer xxx的解析逻辑抽成工具类兼容大小写和多余空格。二是用策略模式对接不同鉴权方式header、query、session 关联三种策略实现同一个接口按配置切换。三是加 Redis 缓存Key 校验结果缓存 5 分钟避免每次请求都查库。注意缓存要设置合理的过期时间Key 吊销后最多 5 分钟生效。四是区分不同 MCP 请求的鉴权粒度tools/list可以宽松tools/call严格校验避免每个请求都做重校验。五是日志脱敏打印 Key 时只保留前 6 位和后 4 位中间用星号替代避免日志泄露。如果你还在选长期编码和 Agent 场景的方案可以了解下 Coding Plan它适合需要持续调用模型和工具的开发流。接入文档在https://taotoken.net/doc里面有各客户端的配置示例。把鉴权链路和统一 Key 通道搭好之后后面加工具、加权限、换模型都只是配置层面的改动不用再动传输层源码。
返回列表