前言
在MCP TypeScript-SDK初级篇中,我们已经掌握了MCP的基础知识,包括开发环境搭建、基础服务器开发、资源开发、工具开发、提示模板开发以及传输层配置等内容。随着我们构建的MCP应用变得越来越复杂,安全问题变得尤为重要。在实际生产环境中,保护敏感资源不被未授权访问是确保系统安全的关键。
本文作为中级篇的第二篇,将深入探讨MCP身份验证与授权实践,包括OAuth集成基础、权限模型设计、访问控制实现以及令牌管理与安全最佳实践。通过学习这些内容,你将能够为MCP应用构建完善的安全机制,保护你的资源和API不被滥用,同时为用户提供便捷的身份验证体验。
一、OAuth集成基础
OAuth 2.0是现代Web应用程序中最广泛使用的授权框架,MCP提供了强大的OAuth集成支持,使应用能够安全地访问用户资源而无需直接处理用户凭证。
1.1 OAuth 2.0概述
在深入MCP的OAuth集成之前,让我们先简要回顾OAuth 2.0的基本概念:
- 资源所有者(Resource Owner): 通常是用户,拥有受保护资源的权限
- 客户端(Client): 请求访问资源的应用程序
- 授权服务器(Authorization Server): 验证资源所有者身份并颁发访问令牌
- 资源服务器(Resource Server): 托管受保护资源的服务器,接受并验证访问令牌
OAuth 2.0定义了几种授权流程,包括:
- 授权码(Authorization Code)流程: 适用于具有后端的Web应用
- 隐式(Implicit)流程: 适用于单页应用(SPA)
- 资源所有者密码凭证(Resource Owner Password Credentials)流程: 适用于可信应用
- 客户端凭证(Client Credentials)流程: 适用于服务器间通信
MCP TypeScript-SDK支持这些标准OAuth流程,同时提供了简化的API使旇集成更加容易。
1.2 在MCP中启用OAuth支持
要在MCP服务器中启用OAuth支持,我们首先需要配置OAuth提供者。MCP TypeScript-SDK提供了灵活的OAuth提供者接口:
import {
McpServer } from '@modelcontextprotocol/sdk';
import {
OAuthProvider, ProxyOAuthServerProvider } from '@modelcontextprotocol/sdk/auth';
import express from 'express';
import {
mcpAuthRouter } from '@modelcontextprotocol/sdk/auth/express';
// 创建MCP服务器实例
const server = new McpServer({
name: 'oauth-enabled-server',
description: 'MCP服务器与OAuth集成示例',
version: '1.0.0',
});
// 创建一个OAuth提供者
const oauthProvider = new ProxyOAuthServerProvider({
endpoints: {
authorizationUrl: 'https://auth.example.com/oauth2/v1/authorize',
tokenUrl: 'https://auth.example.com/oauth2/v1/token',
revocationUrl: 'https://auth.example.com/oauth2/v1/revoke',
// 其他相关端点...
},
// 验证访问令牌的方法
verifyAccessToken: async (token) => {
try {
// 实现验证逻辑,可能涉及调用外部服务
const response = await fetch('https://auth.example.com/oauth2/v1/introspect', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + Buffer.from('client_id:client_secret').toString('base64')
},
body: `token=${
token}`
});
const data = await response.json();
if (data.active) {
return {
token,
clientId: data.client_id,
scopes: data.scope.split(' '),
userId: data.sub,
expiresAt: new Date(data.exp * 1000)
};
}
return null; // 令牌无效
} catch (error) {
console.error('令牌验证失败:', error);
return null;
}
},
// 获取客户端信息的方法
getClient: async (clientId) => {
// 实现客户端查询逻辑
return {
client_id: clientId,
redirect_uris: ['http://localhost:3000/callback'],
// 其他客户端配置...
};
}
});
// 配置服务器使用OAuth提供者
server.useOAuth(oauthProvider);
// 为需要授权的资源添加保护
server.registerResource({
name: 'protected-resource',
description: '受OAuth保护的资源',
requiresAuth: true, // 指定需要授权
requiredScopes: ['read:resource'], // 指定所需的作用域
resolve: async (params, context) => {
// 授权用户信息在context.auth中可用
const {
auth } = context;
return {
content: `这是受保护的资源,只有授权用户可以访问。欢迎用户 ${
auth.userId}!`,
metadata: {
accessInfo: {
clientId: auth.clientId,
scopes: auth.scopes,
userId: auth.userId
}
}
};
}
});
// 创建Express应用并设置OAuth路由
const app = express();
// 使用MCP提供的Auth路由
app.use(mcpAuthRouter({
provider: oauthProvider,
issuerUrl: new URL('https://oauth.example.com'),
baseUrl: new URL('https://api.example.com'),
serviceDocumentationUrl: new URL('https://docs.example.com/oauth-setup'),
}));
// 设置MCP服务器的HTTP传输
// ...其余代码省略
1.3 实现OAuth授权码流程
授权码流程是Web应用最常用的OAuth流程,下面我们实现一个完整的授权码流程:
import express from 'express';
import session from 'express-session';
import {
McpServer } from '@modelcontextprotocol/sdk';
import {
OAuthConfig, AuthorizationCodeFlow } from '@modelcontextprotocol/sdk/auth';
const app = express();
// 配置会话中间件
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production' }
}));
// 创建MCP服务器
const server = new McpServer({
name: 'oauth-server',
description: 'OAuth授权码流程示例',
version: '1.0.0',
});
// OAuth配置
const oauthConfig: OAuthConfig = {
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
authorizationEndpoint: 'https://auth.example.com/oauth2/authorize',
tokenEndpoint: 'https://auth.example.com/oauth2/token',
redirectUri: 'http://localhost:3000/oauth/callback',
scope: 'openid profile email',
};
// 创建OAuth授权码流程
const authCodeFlow = new AuthorizationCodeFlow(oauthConfig);
// 登录页面
app.get('/login', (req, res) => {
// 生成并存储CSRF令牌
const state = Math.random().toString(36).substring(2);
req.session.oauthState = state;
// 重定向到授权URL
const authUrl = authCodeFlow.getAuthorizationUrl({
state });
res.redirect(authUrl);
});
// OAuth回调
app.get('/oauth/callback', async (req, res) => {
try {
const {
code, state } = req.query;
// 验证状态参数,防止CSRF攻击
if (state !== req.session.oauthState) {
throw new Error('无效的状态参数,可能是CSRF攻击');
}
// 清除会话中的状态
delete req.session.oauthState;
// 交换授权码获取令牌
const tokens = await authCodeFlow.exchangeCodeForTokens(code as string);
// 存储令牌
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.tokenExpiry = Date.now() + tokens.expires_in * 1000;
// 获取用户信息
const userInfo = await fetchUserInfo(tokens.access_token);
req.session.user = userInfo;
// 重定向到应用主页
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth回调错误:', error);
res.status(500).send('身份验证失败: ' + error.message);
}
});
// 受保护的路由中间件
function requireAuth(req, res, next) {
if (!req.session.accessToken) {
return res.redirect('/login');
}
// 检查令牌是否过期
if (req.session.tokenExpiry < Date.now()) {
// 实现令牌刷新逻辑
return refreshAccessToken(req, res, next);
}
next();
}
// 受保护的仪表盘页面
app.get('/dashboard', requireAuth, (req, res) => {
res.send(`
<h1>欢迎, ${
req.session.user.name}!</h1>
<p>电子邮件: ${
req.session.user.email}</p>
<a href="/logout">登出</a>
`);
});
// 登出
app.get('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
console.error('会话销毁错误:', err);
}
res.redirect('/');
});
});
// 辅助函数:获取用户信息
async function fetchUserInfo(accessToken) {
const response = await fetch('https://auth.example.com/oauth2/userinfo', {
headers: {
'Authorization': `Bearer ${
accessToken}`
}
});
if (!response.ok) {
throw new Error('获取用户信息失败');
}
return response.json();
}
// 辅助函数:刷新访问令牌
async function refreshAccessToken(req, res, next) {
try {
const newTokens = await authCodeFlow.refreshTokens(req.session.refreshToken);
req.session.accessToken = newTokens.access_token;
req.session.tokenExpiry = Date.now() + newTokens.expires_in * 1000;
// 如果提供了新的刷新令牌,也更新它
if (newTokens.refresh_token) {
req.session.refreshToken = newTokens.refresh_token;
}
next();
} catch (error) {
console.error('令牌刷新失败:', error);
req.session.destroy(err => {
res.redirect('/login');
});
}
}
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${
PORT}`);
});
1.4 集成第三方OAuth提供商
MCP可以轻松集成常见的第三方OAuth提供商。以下是几个常见提供商的集成示例: