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 认证,同时保持向后兼容。
? 升级目标
- 支持 OAuth 2.0 标准: 符合 RFC 8707 和 RFC 8414
- 兼容 Claude Code CLI: 支持其 OAuth 自动发现机制
- 保持向后兼容: 旧的 Headers 认证仍可工作
- 支持多种 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");
}
认证优先级:
- OAuth 2.0 Bearer Token (标准)
- 长期 Token (LTK_ 前缀)
- 自定义 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. 向后兼容策略
渐进式迁移:
- 保留旧的 Headers 认证
- 添加警告日志
- 提供迁移路径
- 文档化新方式
? 测试验证
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] ⚠️ 使用已弃用的自定义头部认证
? 相关文档
? 总结
改造成果
- ✅ OAuth 2.0 标准: 完全符合 RFC 标准
- ✅ Claude Code CLI 兼容: 自动发现和认证
- ✅ 向后兼容: 旧的 Headers 方式仍可工作
- ✅ 长期 Token: 支持持久化配置
- ✅ 多 Client 支持: 各种 MCP Client 都能使用
核心经验
- 标准优先: 遵循 OAuth 2.0 RFC 标准
- 路由优先级: Next.js 静态路由 > 动态路由
- 渐进迁移: 保留旧方式,提供警告和迁移路径
- 用户友好: 提供 Web UI 和自动脚本
- 测试驱动: 全面的兼容性测试
为什么这样改造
Claude Code CLI 的特殊性:
- 严格的 OAuth 2.0 标准要求
- 自动发现机制
- 不支持自定义 Headers
- 期望标准端点路径
其他 MCP Client:
- 可能支持多种认证方式
- 可能使用自定义协议
- 需要向后兼容
我们的方案:
- 标准 OAuth 2.0 作为主路径
- 适配器模式统一处理
- 保留旧方式作为过渡
- 完善的文档和工具
实现时间: 2026-01-15
状态: ✅ 已完成,Claude Code CLI 集成测试通过