【MCP Node.js SDK 全栈进阶指南】中级篇(2):MCP身份验证与授权实践

发布于:2025-05-01 ⋅ 阅读:(13) ⋅ 点赞:(0)

前言

在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定义了几种授权流程,包括:

  1. 授权码(Authorization Code)流程: 适用于具有后端的Web应用
  2. 隐式(Implicit)流程: 适用于单页应用(SPA)
  3. 资源所有者密码凭证(Resource Owner Password Credentials)流程: 适用于可信应用
  4. 客户端凭证(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提供商。以下是几个常见提供商的集成示例:


网站公告

今日签到

点亮在社区的每一天
去签到