MCP 认证升级:从 Headers 到 OAuth 2.0,兼容 Claude Code CLI 的改造之路

2026年01月15日20 次阅读0 人喜欢
MCPOAuth 2.0Claude Code CLI认证升级兼容性Model Context Protocol
所属合集

MCP 认证升级:从 Headers 到 OAuth 2.0,兼容 Claude Code CLI 的改造之路

本文记录了 MCP 服务从自定义 Headers 认证升级到标准 OAuth 2.0 的完整过程,重点解决 Claude Code CLI 与其他 MCP Client 的兼容性问题。

? 项目背景

我们的博客系统最初通过 MCP(Model Context Protocol)提供简单的文章管理功能,使用自定义 Headers 进行认证:

typescript 复制代码
// ❌ 旧方式:自定义 Headers
{
  "x-mcp-account": "admin",
  "x-mcp-password": "password"
}

随着 Claude Code CLI 的普及,我们需要支持标准的 OAuth 2.0 Bearer Token 认证,同时保持向后兼容。

? 升级目标

  1. 支持 OAuth 2.0 标准: 符合 RFC 8707 和 RFC 8414
  2. 兼容 Claude Code CLI: 支持其 OAuth 自动发现机制
  3. 保持向后兼容: 旧的 Headers 认证仍可工作
  4. 支持多种 Client: 不同 MCP Client 都能正常使用

? 改造过程

阶段 1: 初步尝试 - OAuth 2.0 标准端点

问题 1: 路径设计冲突

初始方案:

typescript 复制代码
// ❌ 错误:将 OAuth 端点放在 /api/mcp 下
/api/mcp/.well-known/oauth-protected-resource
/api/mcp/.well-known/oauth-authorization-server

问题:

  • 不符合 OAuth 2.0 标准路径
  • Claude Code CLI 期望 /.well-known/*(根路径)
  • 导致自动发现失败

解决方案:

typescript 复制代码
// ✅ 正确:使用标准根路径
/.well-known/oauth-protected-resource
/.well-known/oauth-authorization-server
/.well-known/openid-configuration

阶段 2: 路由冲突与优先级问题

问题 2: 博客路由捕获系统路径

场景: 当 Claude Code CLI 请求 /.well-known/oauth-protected-resource

日志显示:

复制代码
? 数据库查询执行 - 文章路径: /.well-known/oauth-protected-resource
❌ 文章不存在

原因分析:

博客的动态路由 /[year]/[month]/[date]/[title]/ 会捕获这些路径:

复制代码
请求: /.well-known/oauth-protected-resource
  ↓
Next.js 路由匹配:
  year = ".well-known"
  month = "oauth-protected-resource"
  date = undefined
  title = undefined
  ↓
触发博客路由 → 查询数据库 → 404

解决方案 - Next.js 路由优先级:

复制代码
1. 精确静态路由(最高)
   src/app/.well-known/oauth-protected-resource/route.ts
   
2. Catch-all 动态路由
   src/app/.well-known/[...rest]/route.ts
   
3. 博客动态路由(最低)
   src/app/[year]/[month]/[date]/[title]/page.tsx

博客路由保护:

typescript 复制代码
// src/app/[year]/[month]/[date]/[title]/page.tsx

// 三重防护
if (year.startsWith('.') || year === 'api' || year.startsWith('_')) {
  console.log("⚠️ 检测到系统路径,跳过博客路由");
  return null; // 或 notFound()
}

阶段 3: Claude Code CLI 的 OAuth 自动发现

问题 3: registration_endpoint 导致认证循环

Claude Code CLI 自动发现流程:

复制代码
1. 请求 /.well-known/oauth-protected-resource
   ↓
2. 读取 OAuth 元数据
   ↓
3. 发现 registration_endpoint
   ↓
4. 尝试自动注册客户端
   ↓
5. 失败:401 Unauthorized
   ↓
6. 报错:Authentication required for registration

根本原因:

typescript 复制代码
// OAuth 元数据中包含
{
  "registration_endpoint": "http://localhost:3000/register"
}

// 但 /api/auth/register 需要认证才能访问
// 导致 Claude Code CLI 陷入死循环

解决方案:

typescript 复制代码
// ✅ 从 OAuth 元数据中移除 registration_endpoint
{
  "issuer": "http://localhost:3000",
  "authorization_endpoint": "http://localhost:3000/authorize",
  "token_endpoint": "http://localhost:3000/token",
  // ❌ 移除: registration_endpoint
  // ✅ 保留: 其他标准端点
}

为什么这样设计:

  • OAuth 2.0 标准允许不公开客户端注册
  • 用户需要先登录获取 Token
  • 通过 Web UI 或脚本生成长期 Token
  • 手动配置到 Claude Code CLI

阶段 4: 认证适配器设计

问题 4: 多种认证方式的兼容

需求:

  • Claude Code CLI: OAuth 2.0 Bearer Token
  • 其他 Client: 可能使用自定义 Headers
  • 长期使用: 长期 Token (LTK_ 前缀)
  • 临时使用: 普通 Token (7天有效期)

认证适配器实现:

typescript 复制代码
// src/services/mcpAuth.ts

export async function authenticateMcpRequestEnhanced(headers: Headers) {
  const token = getTokenFromRequest(headers);
  
  if (!token) {
    throw new Error("Missing 'Authorization: Bearer <token>'");
  }

  // 1. 尝试普通 Bearer Token (OAuth 2.0)
  const user = await validateToken(token);
  if (user) return user;

  // 2. 尝试长期 Token (LTK_ 前缀)
  if (token.startsWith('LTK_')) {
    const userId = await validateLongTermToken(token);
    if (userId) {
      const userInfo = await getUserById(userId);
      if (userInfo) return userInfo;
    }
    throw new Error("Invalid or expired long-term token");
  }

  // 3. 向后兼容:自定义 Headers
  const account = headers.get('x-mcp-account');
  const password = headers.get('x-mcp-password');
  if (account && password) {
    console.warn('[MCP] ⚠️ 使用已弃用的自定义 Headers');
    const { login } = await import('@/services/auth');
    const result = await login(account, password);
    if (result) return result.userInfo;
  }

  throw new Error("Invalid or expired token");
}

认证优先级:

  1. OAuth 2.0 Bearer Token (标准)
  2. 长期 Token (LTK_ 前缀)
  3. 自定义 Headers (向后兼容)

阶段 5: MCP 服务端点改造

问题 5: /api/mcp/route.ts 的职责混乱

旧代码:

typescript 复制代码
// ❌ 错误:在一个路由中处理多个路径
export async function GET(request: NextRequest) {
  const url = new URL(request.url);
  
  if (url.pathname === '/api/mcp/.well-known/oauth-protected-resource') {
    return NextResponse.json({ ... });
  }
  if (url.pathname === '/api/mcp/.well-known/oauth-authorization-server') {
    return NextResponse.json({ ... });
  }
  // ... 更多路径判断
}

问题:

  • 违反 Next.js 文件系统路由原则
  • 职责不清晰
  • 难以维护和测试

改造后:

typescript 复制代码
// ✅ 正确:只处理 MCP 服务本身
export async function GET(request: NextRequest) {
  return NextResponse.json({
    status: "active",
    protocol: "mcp",
    version: "2024-11-05",
    authentication: "OAuth 2.0 Bearer Token",
    endpoints: {
      mcp: "/api/mcp",
      oauth_metadata: "/.well-known/oauth-protected-resource",
      oauth_auth_server: "/.well-known/oauth-authorization-server",
      auth: "/api/auth",
      login: "/api/auth/login",
      token: "/api/auth/token",
      // ... 其他端点
    }
  });
}

职责划分:

  • /.well-known/*: OAuth 2.0 元数据
  • /api/mcp: MCP JSON-RPC 服务
  • /api/auth/*: 认证流程

阶段 6: 长期 Token 机制

问题 6: 持久化认证需求

场景: Claude Code CLI 需要长期配置,不能每次重启都重新登录

解决方案:

typescript 复制代码
// 1. 数据库表设计
model LongTermToken {
  id          String    @id @default(uuid())
  token       String    @unique  // LTK_ 前缀
  userId      Int
  expiresAt   DateTime? // null 表示永久
  description String
  createdAt   DateTime  @default(now())
  lastUsed    DateTime?
}

// 2. Token 生成
export async function generateLongTermToken(
  userId: number, 
  duration: number, // 7, 30, 0(永久)
  description: string
) {
  const token = `LTK_${crypto.randomBytes(32).toString('hex')}`;
  
  // 存储到数据库
  await prisma.longTermToken.create({
    data: {
      token,
      userId,
      expiresAt: duration === 0 ? null : new Date(Date.now() + duration * 24 * 60 * 60 * 1000),
      description
    }
  });
  
  return token;
}

// 3. 验证
export async function validateLongTermToken(token: string): Promise<string | null> {
  if (!token.startsWith('LTK_')) return null;
  
  const record = await prisma.longTermToken.findUnique({ where: { token } });
  
  if (!record) return null;
  if (record.expiresAt && new Date() > record.expiresAt) return null;
  
  // 更新最后使用时间
  await prisma.longTermToken.update({
    where: { token },
    data: { lastUsed: new Date() }
  });
  
  return record.userId.toString();
}

用户界面:

typescript 复制代码
// src/components/LongTermTokenCard.tsx
// 提供 Web UI 管理长期 Token
// - 生成新 Token
// - 查看已生成的 Token
// - 复制 Token
// - 删除 Token

阶段 7: OAuth 2.0 Token 端点

问题 7: 标准 OAuth Token 流程

需求: 支持标准的 OAuth 2.0 Token 获取方式

实现:

typescript 复制代码
// src/services/mcpAuth.ts

export async function handleOAuthTokenRequest(request: NextRequest) {
  const body = await parseTokenRequestBody(request);
  const { grant_type, client_id, code, redirect_uri, code_verifier } = body;

  switch (grant_type) {
    case 'client_credentials':
      // 客户端凭证模式
      return await handleClientCredentialsGrant(request);
      
    case 'authorization_code':
      // 授权码模式(支持 PKCE)
      return await handleAuthorizationCodeGrant(code, client_id, redirect_uri, code_verifier);
      
    default:
      return NextResponse.json({
        error: 'unsupported_grant_type',
        error_description: `Grant type '${grant_type}' not supported`
      }, { status: 400 });
  }
}

// 客户端凭证模式(最简单)
async function handleClientCredentialsGrant(request: NextRequest) {
  const token = getTokenFromRequest(request.headers);
  
  if (!token) {
    return NextResponse.json({
      error: 'invalid_client',
      error_description: 'Client credentials required'
    }, { status: 401 });
  }

  // 验证 Token
  const user = await validateToken(token);
  if (!user) {
    return NextResponse.json({
      error: 'invalid_client',
      error_description: 'Invalid client credentials'
    }, { status: 401 });
  }

  // 返回 OAuth 标准响应
  return NextResponse.json({
    access_token: token,
    token_type: 'Bearer',
    expires_in: 7 * 24 * 60 * 60, // 7天
    scope: 'read write'
  });
}

?️ 完整认证流程

Claude Code CLI 配置流程

复制代码
第一步:用户登录
  ↓
POST /api/auth/login
  ↓
获取普通 Token (7天有效期)
  ↓

第二步:生成长期 Token
  ↓
访问 /c/user/info
  ↓
在"长期Token管理"卡片中生成
  ↓
选择有效期:永久/7天/30天
  ↓
获取 LTK_xxx 格式的长期 Token
  ↓

第三步:配置 Claude Code CLI
  ↓
claude mcp add MyBlog http://localhost:3000/api/mcp \
  --header "Authorization: Bearer LTK_xxx"
  ↓

第四步:自动发现(Claude Code CLI 内部)
  ↓
请求 /.well-known/oauth-protected-resource
  ↓
读取 OAuth 元数据
  ↓
发现没有 registration_endpoint
  ↓
直接使用提供的 Bearer Token
  ↓

第五步:成功连接
  ↓
可以使用 MCP 工具了

其他 MCP Client 配置流程

复制代码
方案 A: 使用 OAuth 2.0 Bearer Token
  ↓
同 Claude Code CLI 流程
  ↓

方案 B: 使用自定义 Headers(向后兼容)
  ↓
claude mcp add MyBlog http://localhost:3000/api/mcp \
  --header "x-mcp-account: admin" \
  --header "x-mcp-password: password"
  ↓
适配器自动转换为标准认证
  ↓
显示警告:建议迁移到 OAuth 2.0

? 兼容性问题与解决方案

问题 1: 不同 Client 的认证方式差异

Client 类型 认证方式 问题 解决方案
Claude Code CLI OAuth 2.0 Bearer Token 需要标准端点 提供 /.well-known/*
自定义 Client Headers 旧方式 适配器向后兼容
浏览器 Cookie/Session 需要 Web UI 提供管理界面

问题 2: Token 格式统一

问题: 不同来源的 Token 需要统一验证

解决方案:

typescript 复制代码
// 统一验证入口
export async function validateToken(token: string): Promise<User | null> {
  // 1. 普通 Token: 存储在 Redis user:${token}
  let userStr = await RedisService.get(`user:${token}`);
  if (userStr) return JSON.parse(userStr);

  // 2. OAuth Token: 存储在 Redis token:${token}
  const tokenDataStr = await RedisService.get(`token:${token}`);
  if (tokenDataStr) {
    const tokenData = JSON.parse(tokenDataStr);
    const user = await getUserById(tokenData.userId);
    if (user) return user;
  }

  // 3. 长期 Token: 存储在数据库
  if (token.startsWith('LTK_')) {
    const userId = await validateLongTermToken(token);
    if (userId) {
      return await getUserById(parseInt(userId));
    }
  }

  return null;
}

问题 3: OAuth 元数据完整性

问题: Claude Code CLI 期望完整的 OAuth 2.0 元数据

解决方案:

typescript 复制代码
// src/app/.well-known/oauth-protected-resource/route.ts

export async function GET(request: NextRequest) {
  const url = new URL(request.url);
  
  return NextResponse.json({
    issuer: url.origin,
    authorization_endpoint: `${url.origin}/authorize`,
    token_endpoint: `${url.origin}/token`,
    revocation_endpoint: `${url.origin}/revoke`,
    introspection_endpoint: `${url.origin}/introspect`,
    scopes_supported: ['read', 'write', 'admin'],
    response_types_supported: ['token', 'code'],
    grant_types_supported: ['client_credentials', 'authorization_code'],
    token_endpoint_auth_methods_supported: ['bearer', 'none'],
    code_challenge_methods_supported: ['plain', 'S256'],
    resource: 'React Blog MCP',
    service_documentation: 'https://github.com/NNNNzs/react.nnnnzs.cn'
  }, {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, max-age=3600'
    }
  });
}

? 改造前后对比

认证方式

方面 改造前 改造后
认证协议 自定义 Headers OAuth 2.0 标准
Token 格式 无结构 Bearer / LTK_ 前缀
兼容性 仅自定义 Client 所有标准 Client
自动发现 不支持 完全支持
长期配置 不支持 支持长期 Token

路由结构

端点 改造前 改造后
OAuth 元数据 /api/mcp/.well-known/* /.well-known/*
MCP 服务 /api/mcp /api/mcp
认证端点 /api/auth/* /api/auth/*
路由冲突 频繁发生 完全解决

Claude Code CLI 兼容性

功能 改造前 改造后
自动发现 ❌ 失败 ✅ 成功
自动注册 ❌ 认证循环 ✅ 移除,手动配置
Token 验证 ❌ 不支持 ✅ 完全支持
工具调用 ❌ 需要改造 ✅ 直接使用

? 核心技术点

1. Next.js 路由优先级

关键: 静态路由永远优先于动态路由

复制代码
/.well-known/oauth-protected-resource/route.ts  ← 最高优先级
/.well-known/[...rest]/route.ts                ← 次之
/[year]/[month]/[date]/[title]/page.tsx        ← 最低

2. OAuth 2.0 标准兼容

遵循标准:

  • RFC 8707: OAuth 2.0 Protected Resource Metadata
  • RFC 8414: OAuth 2.0 Authorization Server Metadata
  • RFC 7636: PKCE (Proof Key for Code Exchange)

移除非标准:

  • registration_endpoint: 导致自动注册循环
  • 自定义 Headers: 不符合标准

3. 多层认证适配

适配器模式:

typescript 复制代码
// 输入: 各种认证方式
// 输出: 统一的 User 对象

authenticateMcpRequestEnhanced(headers)
  ↓
  ├─ Bearer Token → validateToken
  ├─ LTK Token → validateLongTermToken
  └─ Headers → login (带警告)
  ↓
User 对象

4. 向后兼容策略

渐进式迁移:

  1. 保留旧的 Headers 认证
  2. 添加警告日志
  3. 提供迁移路径
  4. 文档化新方式

? 测试验证

1. OAuth 端点测试

bash 复制代码
# 应该返回标准 OAuth 元数据(不含 registration_endpoint)
curl http://localhost:3000/.well-known/oauth-protected-resource

# 预期输出
{
  "issuer": "http://localhost:3000",
  "authorization_endpoint": "http://localhost:3000/authorize",
  "token_endpoint": "http://localhost:3000/token",
  "scopes_supported": ["read", "write", "admin"],
  "grant_types_supported": ["client_credentials", "authorization_code"],
  "code_challenge_methods_supported": ["plain", "S256"]
}

2. Claude Code CLI 集成测试

bash 复制代码
# 1. 生成 Token
./scripts/get-claude-token.sh

# 2. 配置
claude mcp add MyBlog http://localhost:3000/api/mcp \
  --header "Authorization: Bearer LTK_xxx"

# 3. 验证
claude mcp list
# 应该显示: ✓ connected & authenticated

# 4. 测试工具
# 在 Claude Code 中输入: "请列出最近的文章"

3. 兼容性测试

bash 复制代码
# 旧方式(应该仍可工作,但有警告)
curl -X POST http://localhost:3000/api/mcp \
  -H "x-mcp-account: admin" \
  -H "x-mcp-password: password" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

# 检查日志应该看到警告
# [MCP] ⚠️ 使用已弃用的自定义头部认证

? 相关文档

? 总结

改造成果

  1. OAuth 2.0 标准: 完全符合 RFC 标准
  2. Claude Code CLI 兼容: 自动发现和认证
  3. 向后兼容: 旧的 Headers 方式仍可工作
  4. 长期 Token: 支持持久化配置
  5. 多 Client 支持: 各种 MCP Client 都能使用

核心经验

  1. 标准优先: 遵循 OAuth 2.0 RFC 标准
  2. 路由优先级: Next.js 静态路由 > 动态路由
  3. 渐进迁移: 保留旧方式,提供警告和迁移路径
  4. 用户友好: 提供 Web UI 和自动脚本
  5. 测试驱动: 全面的兼容性测试

为什么这样改造

Claude Code CLI 的特殊性:

  • 严格的 OAuth 2.0 标准要求
  • 自动发现机制
  • 不支持自定义 Headers
  • 期望标准端点路径

其他 MCP Client:

  • 可能支持多种认证方式
  • 可能使用自定义协议
  • 需要向后兼容

我们的方案:

  • 标准 OAuth 2.0 作为主路径
  • 适配器模式统一处理
  • 保留旧方式作为过渡
  • 完善的文档和工具

实现时间: 2026-01-15
状态: ✅ 已完成,Claude Code CLI 集成测试通过

加载评论中...