ARTICLE DETAIL

资讯详情

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

大模型开发 - 40 MCP:基于Spring AI和OAuth2的MCP授权全流程实战(TaoToken统一Key接入版)

大模型开发 - 40 MCP:基于Spring AI和OAuth2的MCP授权全流程实战(TaoToken统一Key接入版) 1. 为什么 MCP 服务端必须补上 OAuth2 这一环MCP 刚火起来那阵子我见过太多项目把工具接口直接裸奔在 HTTP 上本地跑跑没问题一旦要接企业内网的数据源安全同学第一句话就是「这个接口谁能调」。Model Context Protocol 本身解决的是「大模型怎么发现并调用工具」的问题它并没有规定鉴权怎么做。换句话说MCP 把工具描述、参数 schema、调用协议都标准化了但「谁有权限调用哪个工具」这件事协议层是留白的。这就是 OAuth2 要补位的地方。你可以把 MCP 服务端理解成一个资源服务器它暴露的每一个 tool比如查订单、算金额、读工单都是受保护资源。客户端拿着授权服务器签发的 JWT 来访问服务端只负责验签和校验 scope业务代码里完全不用写 if-else 判断用户身份。这种「认证与业务解耦」的设计在 Spring AI 体系里落地起来其实相当顺因为 Spring Security 的 OAuth2 资源服务器支持是现成的。这篇要交付的东西很具体一个能跑通的授权服务器、一个带计算器工具的 MCP 服务端、一个能区分「用户授权码令牌」和「系统客户端凭证令牌」的 MCP 客户端。同时我会把大模型调用的 API 通道统一到 TaoToken 上这样你本地不用维护一堆厂商的 Key一个统一 Key 就能把 Claude、GPT 这些模型接进来专注在 OAuth2 链路的调试上。适合谁看正在用 Spring AI 做 MCP 服务端、并且被「怎么加鉴权」卡住的同学或者你已经跑通了 MCP 的 stdio 版本现在想升级到带 SSE 的远程安全版本。先说清楚整体架构不然后面配置容易迷路。系统里有三个独立进程授权服务器跑在 9000 端口负责发令牌MCP 服务端跑在 8090扮演资源服务器暴露计算器工具MCP 客户端跑在 8080它既是 Web 应用又是 MCP 客户端负责拿令牌、调工具、再驱动大模型。客户端这里有个容易忽略的点——它启动时要用客户端凭证模式拿一个「系统级」令牌去初始化 MCP 连接而用户通过浏览器访问时又要用授权码模式拿「用户级」令牌。两套令牌动态切换是整条链路里最容易踩坑的地方后面我会用自定义的 ExchangeFilterFunction 来解决。2. TaoToken 统一 Key 的前置准备与模型通道配置在动手写 OAuth2 之前先把大模型这条通道理顺。MCP 客户端最终是要驱动大模型去决定「该调哪个工具」的所以你得有一个能用的模型 API。传统做法是去各家厂商注册、拿 Key、配不同的 SDK光是环境变量就一堆。我现在的习惯是统一走 TaoToken 的 API 通道一个 Key 覆盖多个模型省得在 OAuth2 调试时分心去处理模型鉴权。你需要先拿到两样东西一个是 API Key一个是确认好要用的模型 ID。Key 在控制台的 API Keys 页面创建地址是 https://taotoken.net/api-keys 创建后复制出来形如sk-开头的一串。模型 ID 则可以在模型对话页面里先试一下确认你要用的模型能正常返回页面在 https://taotoken.net/chat 。这一步别跳过因为后面 Spring AI 配置里要填的 model 名称必须和平台一致填错了会报模型不存在的错。拿到 Key 之后建议用环境变量的方式注入不要硬编码进配置文件。Linux/macOS 下这样设置export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiWindows PowerShell 用$env:TAOTOKEN_API_KEYsk-你的key $env:TAOTOKEN_BASE_URLhttps://taotoken.net/api这里有个关键点TaoToken 的 API 地址是https://taotoken.net/api它兼容 OpenAI 的接口规范所以 Spring AI 里可以直接用 OpenAI 的 starter只要把 base-url 指过来就行。如果你用的是 Anthropic 协议那套Spring AI 也有对应的 starter但为了演示统一我这里用 OpenAI 兼容模式配置最省事。先验证一下 Key 是否可用用 curl 打一个最简请求curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: claude-3-5-sonnet-20241022, messages: [{role: user, content: reply with ok}], max_tokens: 16 }如果返回里有choices数组且 content 是 ok 之类的内容说明通道没问题。这一步很重要因为后面 MCP 客户端调模型失败时你要能快速区分是「模型通道挂了」还是「OAuth2 令牌没拿到」。我踩过的坑就是有一次令牌链路全对但模型 base-url 写成了官方地址结果一直超时排查了半天才发现是通道配错。关于模型选择MCP 场景下工具调用能力比较关键建议选支持 function calling 的模型。你可以在模型对话页面里对比几个模型的工具调用表现选一个稳定的写进配置。Coding Plan 那边也有针对长期编码和 Agent 场景的套餐如果你打算把 MCP 服务端长期跑着做开发助手可以看看 https://taotoken.net/coding-plan 按需选就行这里不展开。前置准备做完你应该有一个可用的 API Key、一个确认能返回的模型 ID、以及验证通过的 curl 结果。接下来进入 OAuth2 授权服务器的搭建。3. 可复制的 OAuth2 授权服务器与 MCP 服务端配置这一节是全文的核心我会把授权服务器、MCP 服务端、MCP 客户端三份配置都给全你直接复制改端口就能用。先说授权服务器它是最独立的模块跑起来之后其他两个都依赖它发令牌。授权服务器的依赖就两个spring-boot-starter-oauth2-authorization-server和spring-boot-starter-web。配置文件用 YAML注意 client 注册那块我注册了一个mcp-client同时开了授权码、客户端凭证、刷新令牌三种模式scope 里除了 openid/profile还加了calc.read和calc.write这两个是给计算器工具用的自定义 scopeserver: port: 9000 spring: security: user: name: user password: password oauth2: authorizationserver: client: oidc-client: registration: client-id: mcp-client client-secret: {noop}mcp-secret client-authentication-methods: - client_secret_basic authorization-grant-types: - authorization_code - client_credentials - refresh_token redirect-uris: - http://localhost:8080/authorize/oauth2/code/authserver scopes: - openid - profile - calc.read - calc.write注意{noop}前缀这是告诉 Spring Security 这个 secret 是明文生产环境要换成 BCrypt 编码。redirect-uri 必须和客户端配置里的完全一致差一个字符都会报 redirect_uri_mismatch。然后是 MCP 服务端它要同时引入 MCP 服务端 starter 和 OAuth2 资源服务器 starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId version1.0.0-M7/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-oauth2-resource-server/artifactId /dependency服务端配置里最关键的一行是issuer-uri它指向授权服务器Spring Boot 会自动去拉 JWK Set 来验签server.port8090 spring.security.oauth2.resourceserver.jwt.issuer-urihttp://localhost:9000 spring.ai.mcp.server.enabledtrue spring.ai.mcp.server.namemcp-calculator-server spring.ai.mcp.server.version1.0.0 spring.ai.mcp.server.stdiofalsestdiofalse表示走 SSE 远程模式这样才能配合 OAuth2 做 HTTP 鉴权。工具实现用Tool注解两个算术方法Tool(description Add two numbers) public CalculationResult add( ToolParam(description First number) double a, ToolParam(description Second number) double b) { double result a b; return new CalculationResult(addition, a, b, result); } Tool(description Multiply two numbers) public CalculationResult multiply( ToolParam(description First number) double a, ToolParam(description Second number) double b) { double result a * b; return new CalculationResult(multiplication, a, b, result); }安全配置会自动把这些工具方法保护起来没有合法 JWT 的请求直接 401。这里不需要你手写过滤器资源服务器 starter 会接管。最后是 MCP 客户端它最复杂因为要配两套 client registration。一套给用户授权码模式一套给系统客户端凭证模式server.port8080 spring.ai.mcp.client.sse.connections.server1.urlhttp://localhost:8090 spring.ai.mcp.client.typeSYNC spring.security.oauth2.client.provider.authserver.issuer-urihttp://localhost:9000 # 用户授权码模式 spring.security.oauth2.client.registration.authserver.client-idmcp-client spring.security.oauth2.client.registration.authserver.client-secretmcp-secret spring.security.oauth2.client.registration.authserver.authorization-grant-typeauthorization_code spring.security.oauth2.client.registration.authserver.providerauthserver spring.security.oauth2.client.registration.authserver.scopeopenid,profile,calc.read,calc.write spring.security.oauth2.client.registration.authserver.redirect-uri{baseUrl}/authorize/oauth2/code/{registrationId} # 系统客户端凭证模式 spring.security.oauth2.client.registration.authserver-client-credentials.client-idmcp-client spring.security.oauth2.client.registration.authserver-client-credentials.client-secretmcp-secret spring.security.oauth2.client.registration.authserver-client-credentials.authorization-grant-typeclient_credentials spring.security.oauth2.client.registration.authserver-client-credentials.providerauthserver spring.security.oauth2.client.registration.authserver-client-credentials.scopecalc.read,calc.write # 模型通道走 TaoToken spring.ai.openai.api-key${TAOTOKEN_API_KEY} spring.ai.openai.base-url${TAOTOKEN_BASE_URL} spring.ai.openai.chat.options.modelclaude-3-5-sonnet-20241022注意 scope 这里我写的是calc.read,calc.write和授权服务器里注册的保持一致。模型那三行就是 TaoToken 的接入点base-url 指向https://taotoken.net/apiKey 从环境变量读。这样模型通道和 OAuth2 通道就都配齐了。4. 动态令牌切换与 curl 验证授权全流程配置写完不代表能跑客户端这里有个隐蔽的坑MCP 客户端在应用启动时就要建立 SSE 连接这时候还没有用户登录拿不到授权码令牌所以必须用客户端凭证模式先拿一个系统令牌。而用户通过浏览器访问/calculate时又要用当前登录用户的授权码令牌。两套令牌怎么在同一个 WebClient 里动态切换答案是自定义 ExchangeFilterFunction。先看安全配置放行所有请求但启用 oauth2ClientBean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(auth - auth.anyRequest().permitAll()) .oauth2Client(Customizer.withDefaults()) .csrf(CsrfConfigurer::disable) .build(); }核心的过滤器实现如下逻辑是如果当前线程有 ServletRequestAttributes说明是用户请求就用 delegate 走授权码令牌否则启动初始化阶段就用客户端凭证模式现拿一个令牌塞进 headerComponent public class McpSyncClientExchangeFilterFunction implements ExchangeFilterFunction { private final ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialTokenProvider new ClientCredentialsOAuth2AuthorizedClientProvider(); private final ServletOAuth2AuthorizedClientExchangeFilterFunction delegate; private final ClientRegistrationRepository clientRegistrationRepository; private static final String AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID authserver; private static final String CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID authserver-client-credentials; public McpSyncClientExchangeFilterFunction(OAuth2AuthorizedClientManager clientManager, ClientRegistrationRepository clientRegistrationRepository) { this.delegate new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientManager); this.delegate.setDefaultClientRegistrationId(AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID); this.clientRegistrationRepository clientRegistrationRepository; } Override public MonoClientResponse filter(ClientRequest request, ExchangeFunction next) { if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) { return this.delegate.filter(request, next); } else { var accessToken getClientCredentialsAccessToken(); var requestWithToken ClientRequest.from(request) .headers(headers - headers.setBearerAuth(accessToken)) .build(); return next.exchange(requestWithToken); } } private String getClientCredentialsAccessToken() { var clientRegistration this.clientRegistrationRepository .findByRegistrationId(CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID); var authRequest OAuth2AuthorizationContext.withClientRegistration(clientRegistration) .principal(new AnonymousAuthenticationToken(client-credentials-client, client-credentials-client, AuthorityUtils.createAuthorityList(ROLE_ANONYMOUS))) .build(); return this.clientCredentialTokenProvider.authorize(authRequest).getAccessToken().getTokenValue(); } public ConsumerWebClient.Builder configuration() { return builder - builder.defaultRequest(this.delegate.defaultRequest()).filter(this); } }这段代码是整个链路里最值得反复读的部分。RequestContextHolder.getRequestAttributes()这个判断是分水岭启动阶段没有请求上下文走 else 分支用户请求进来时有上下文走 delegate。如果你发现启动时报 401八成是这个判断没生效或者客户端凭证的 registration id 写错了。配置好之后启动顺序不能乱先起授权服务器9000再起 MCP 服务端8090最后起 MCP 客户端8080。服务端启动时会去 9000 拉 JWK如果授权服务器没起来服务端会启动失败。验证分两步。第一步直接用 curl 走客户端凭证模式拿令牌确认授权服务器正常curl -X POST http://localhost:9000/oauth2/token \ -u mcp-client:mcp-secret \ -d grant_typeclient_credentials \ -d scopecalc.read calc.write返回里应该有access_token字段。第二步拿这个令牌去调 MCP 服务端的工具接口验证资源服务器鉴权生效TOKEN$(curl -s -X POST http://localhost:9000/oauth2/token \ -u mcp-client:mcp-secret \ -d grant_typeclient_credentials \ -d scopecalc.read calc.write | jq -r .access_token) curl -H Authorization: Bearer $TOKEN http://localhost:8090/sse如果返回 SSE 事件流而不是 401说明令牌校验通过。再故意不带令牌请求一次应该返回 401这样一正一反就验证了鉴权确实生效。最后走完整链路浏览器访问http://localhost:8080/calculate?expression1525客户端会引导你登录授权服务器登录后拿到授权码令牌再带着令牌去调 MCP 服务端的计算器工具大模型根据工具返回结果组织答案。整个过程你能在日志里看到令牌的获取和携带。5. 常见报错排查401、local proxy failed 与 OAuth2 授权码异常链路跑通之前报错是常态。我把几个高频错误和对应排查路径列出来你对着日志定位会快很多。第一个是401 Unauthorized出现在 MCP 服务端。原因通常是三种令牌没带、令牌过期、或者 issuer 不匹配。先看请求 header 里有没有Authorization: Bearer xxx没有就是客户端过滤器没生效有的话把令牌贴到 jwt.io 解一下看iss字段是不是http://localhost:9000和资源服务器配的 issuer-uri 必须完全一致差个斜杠都会验签失败。还有一种情况是时钟偏移JWT 的exp和iat对时间敏感容器时间不对也会 401。第二个是local proxy failed或者连接被拒。这个多半是启动顺序问题MCP 服务端启动时去拉授权服务器的 JWK如果 9000 还没起来就会报连接失败。解决办法就是严格按 9000 → 8090 → 8080 的顺序启动或者给服务端加个重试。另外检查一下spring.security.oauth2.resourceserver.jwt.issuer-uri有没有写成https本地是http写错协议也会连不上。第三个是reading choices相关的报错这个出在模型通道。MCP 客户端调 TaoToken 时如果返回体里没有choices字段通常是 base-url 或 model 名写错了。检查spring.ai.openai.base-url是不是https://taotoken.net/api注意结尾不要多加/v1Spring AI 会自己拼。model 名要和平台一致写错了会返回错误对象而不是 choices 数组。这时候用第 2 节的 curl 命令再验一次能快速区分是通道问题还是代码问题。第四个是 OAuth2 授权码模式的redirect_uri_mismatch或invalid_client。redirect-uri 必须和授权服务器注册的完全一致包括端口和路径。invalid_client一般是 client-secret 错了注意授权服务器里配的是{noop}mcp-secret客户端里填的应该是mcp-secret不要带{noop}前缀。还有 scope 不匹配也会报错客户端请求的 scope 必须是授权服务器注册过的子集。第五个是启动时 MCP 客户端报OAuth2AuthorizationContext相关异常。这通常是客户端凭证模式的 registration id 和代码里的常量对不上。检查CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID是不是authserver-client-credentials和 properties 里的前缀一致。这个 id 是spring.security.oauth2.client.registration.后面那一段写错了就找不到注册信息。排查的时候有个通用技巧把 Spring Security 的日志级别调到 DEBUG在 properties 里加logging.level.org.springframework.securityDEBUG令牌的获取、校验、拒绝过程都会打出来比猜快得多。另外 TaoToken 的接入文档在 https://taotoken.net/doc 模型通道相关的参数对照着看能省不少时间。6. 把 MCP 鉴权链路沉淀成可复用模板走到这里你应该已经跑通了「授权服务器发令牌 → MCP 服务端验令牌 → 客户端动态切令牌 → 大模型驱动工具调用」的完整闭环。回头看OAuth2 在这套体系里的价值不是增加复杂度而是把「谁能调什么」这件事从业务代码里抽出来交给标准协议处理。你后面要加新工具只需要在Tool方法上写注解鉴权自动生效不用改一行安全代码。几个可以立刻用起来的经验。第一把三份配置抽成独立的 profile本地用application-local.properties测试环境换 issuer-uri 就行代码不用动。第二客户端凭证的 scope 建议只给读权限用户授权码模式再给写权限最小权限原则在 MCP 场景同样适用。第三模型通道统一走 TaoToken 之后你换模型只改一行spring.ai.openai.chat.options.modelOAuth2 链路完全不受影响这种解耦在调试期特别省心。如果你打算把这套东西用到生产下一步可以考虑把授权服务器换成支持持久化的实现客户端注册信息从数据库读而不是写死在 YAML 里。MCP 服务端这边工具方法里的业务逻辑记得做参数校验和异常兜底因为大模型传过来的参数不一定符合预期。至于模型侧长期跑 Agent 的话可以关注一下 Coding Plan 的额度方案按调用量选比按次买划算。最后留一个可以直接复用的验证脚本把三端启动和令牌校验串起来每次改完配置跑一遍比手动点浏览器快#!/bin/bash set -e echo 1. 获取客户端凭证令牌 TOKEN$(curl -s -X POST http://localhost:9000/oauth2/token \ -u mcp-client:mcp-secret \ -d grant_typeclient_credentials \ -d scopecalc.read calc.write | jq -r .access_token) echo token: ${TOKEN:0:20}... echo 2. 带令牌访问 MCP 服务端 curl -s -o /dev/null -w %{http_code}\n \ -H Authorization: Bearer $TOKEN http://localhost:8090/sse echo 3. 不带令牌应返回 401 curl -s -o /dev/null -w %{http_code}\n http://localhost:8090/sse echo 4. 验证模型通道 curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:claude-3-5-sonnet-20241022,messages:[{role:user,content:ok}],max_tokens:8} \ | jq -r .choices[0].message.content这个脚本跑完四步结果都对说明整条链路是健康的。哪一步挂了就回到对应章节查配置。MCP 加 OAuth2 这套组合第一次配会觉得环节多但配通一次之后就是模板后面加工具、换模型都是改配置的事。
返回列表