news 2026/9/10 8:00:14

Serverless Framework 集成测试的 Cognito 前置依赖:为 MCP 鉴权与发现测试套件预置一次性的 M2M 用户池

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Serverless Framework 集成测试的 Cognito 前置依赖:为 MCP 鉴权与发现测试套件预置一次性的 M2M 用户池

Serverless Framework 集成测试的 Cognito 前置依赖:为 MCP 鉴权与发现测试套件预置一次性的 M2M 用户池

【免费下载链接】serverless⚡ Serverless Framework – Effortlessly build apps that auto-scale, incur zero costs when idle, and require minimal maintenance using AWS Lambda and other managed cloud services.项目地址: https://gitcode.com/GitHub_Trending/se/serverless

本指南以 mcp-cognito-prerequisite/README.md 为骨架,深入讲解 Serverless Framework(sf-core)在真实 AWS 集成测试中如何用一个"一次性、持久、按账户部署"的 Cognito 用户池来支撑 MCP 属性的 enforcement-and-discovery 测试套件:包括模板预置了什么资源、为什么必须用自定义资源把密钥写成 SSM SecureString、部署/校验/拆除的命令,以及测试套件如何从 SSM 运行时发现一切、做到"零硬编码、无前置则干净跳过、读取出错则响亮失败"。读完后你可以复现这套部署流程,并理解client_credentialsM2M 鉴权测试中 issuer、token endpoint、scope 与资源服务器之间的真实关系。

这套前置依赖在项目中的定位

Serverless Framework 的mcp属性本身不做鉴权——这是 mcp-auth.test.js 开头就点明的核心立场:"Enforcement is the user's"(执行鉴权是用户的责任)。框架只负责把用户配置的 authorizer 编译成 API Gateway 形态,并可选地在 API Gateway MOCK 路由上发布一份 RFC 9728 protected-resource 文档,供交互式 MCP 客户端发现登录入口。

为了在真实 AWS 上证明这一立场,测试套件需要部署四类受不同访问控制保护的 MCP 服务器。其中cognito这一类需要一个真实存在的 Cognito 用户池做 API Gateway Cognito authorizer 的授权主体。由于用户池 id、域名、客户端密钥等在编写测试时无法预先知道(且不应硬编码进仓库),项目采用了一个独立于测试套件、每次运行自生自灭的 fixture 栈之外的持久化前置依赖:即本目录中的template.yml预置的 Cognito 用户池。

按 TESTING.md 的编排,只有mcp-auth.test.js(enforcement-and-discovery 套件)需要这个 Cognito 前置依赖;缺少它时该套件会打印日志后跳过,其余 MCP 套件(如mcp.test.js)不受影响。

设计原则:一次性部署、持久存在、运行时发现

前置依赖设计上有三条硬约束:

  • 持久且按账户一次性部署:用纯 CloudFormation 部署一次后长期保留,测试套件在运行时从 SSM 读取全部标识,因此没有硬编码任何 id。
  • 共享同一 fixture 目录在 jest 并行下是竞态:因此本前置与套件分离,套件自己的 fixture 位于 fixture-auth,每套测试独占一个目录。
  • 缺失时干净跳过、损坏时响亮失败:套件只在 SSM 前缀真正不可用(空、不完整或完全没有凭证)时跳过;被拒绝、被限流或超时的读取会直接导致任务失败——那意味着 CI 环境坏了,而不是账户主动退出测试。

template.yml 预置了什么

模板位于 template.yml,是一个标准AWSTemplateFormatVersion: '2010-09-09'的 CloudFormation 模板(注意:是纯 CloudFormation,而非 Serverless 服务定义)。它包含三个可覆盖的模板参数:

参数默认值说明
SsmPrefix/mcp-integration-test/cognitoSSM 参数前缀,池 id、域名、region、两个客户端 id/secret 与 scope 都写在此前缀之下,必须与 cognito.mjs 中导出的DEFAULT_PREFIX保持一致
ResourceServerIdentifiermcp资源服务器标识,与ScopeName拼接成完整 scope
ScopeNameinvoke自定义 scope 名,完整 scope 字符串形如<identifier>/<name>

其预置的核心资源包括:

  1. 一个 Lite 层的用户池AWS::Cognito::UserPoolUserPoolName: mcp-integration-test)。Lite 层足够的原因在模板注释中写得很清楚:套件部署的受保护服务器配置了自定义 scope,正是 scope 让 API Gateway 校验该池签发的原始 access token,因此无需 token 定制/pre-token-generation 触发器。
  2. 一个用户池域名mcp-integration-test-<account-id>AWS::Cognito::UserPoolDomain)。域名按账户 id 命名以保证全局唯一,同时它承载/oauth2/token端点,套件正是用它铸币(mint)token。
  3. 一个资源服务器mcpAWS::Cognito::UserPoolResourceServer),带一个自定义 scopeinvoke→ 完整 scope 字符串mcp/invoke,scope 描述为 "Invoke the MCP server"。
  4. 两个应用客户端 Client A / Client BAWS::Cognito::UserPoolClient),均为client_credentialsM2M 客户端:
    • GenerateSecret: true,由 CloudFormation 生成客户端密钥;
    • AllowedOAuthFlows: [client_credentials]AllowedOAuthScopes限定为mcp/invoke
    • SupportedIdentityProviders: [COGNITO]
    • 二者都DependsOn: ResourceServer,确保 scope 先于客户端创建。

两个客户端的角色差异是这套测试的"考点"之一:

  • Client A:套件铸造工作 token(working token)的客户端。
  • Client B:同一池、同一 scope,但不同 client id。测试套件断言它的 token同样被接受——因为 API Gateway 的 Cognito authorizer 的授权粒度是"池 + scope",永远不是某一个客户端。固定这一个断言,就能阻止读者误以为网关在客户端层面做了更细的收敛。真正要收窄到单个客户端,是服务器模块的职责(token 的client_id声明会随 authorizer context 到达服务器),这恰好印证了"enforcement is yours"的边界(见 mcp-auth.test.js)。

密钥如何进 SSM:为什么必须自定义资源

CloudFormation 原生的AWS::SSM::Parameter无法创建 SecureString 类型参数——这是模板注释和 README 共同强调的限制。解决办法是模板内联(ZipFile)了一个极小的 Python 3.12 Lambda 自定义资源SsmWriterFunction,栈名派生为<stack-name>-ssm-writer

它发布的八个 SecureString 参数位于/mcp-integration-test/cognito/下:poolIddomainregionclientAIdclientASecretclientBIdclientBSecretscope。这八个键名与 cognito.mjs 里的REQUIRED_KEYS逐一对应——八个必须全部存在,前置依赖才视为已部署。

几个值得注意的安全与生命周期细节:

  • 客户端密钥只存在于 SSM,从不进入栈的 Outputs。模板的 Outputs 只回显非敏感发现值(UserPoolIdDomainRegionClientAIdClientBIdScopeSsmPrefix),供管理员部署后目测。
  • 自定义资源的 IAM 最小化SsmWriterRole只允许ssm:PutParameter/ssm:DeleteParameter,且资源被限定为arn:...:parameter${SsmPrefix}/*;日志写权限只针对该函数自己显式声明的日志组/aws/lambda/${AWS::StackName}-ssm-writerRetentionInDays: 7)。因为日志组是显式声明的而非首次调用时自动创建,函数名(由栈名派生)在创建角色时就可确定,不存在依赖环,也完全不需要logs:CreateLogGroup
  • 删除即清理:自定义资源在RequestType == 'Delete'时逐个delete_parameter(对ParameterNotFound静默容错);Create/Update则用Overwrite: True覆盖写入。因此拆除整个栈就会连带清掉八个 SSM 参数,不会遗留凭据。
  • 密钥通过!GetAtt UserPoolClientA.ClientSecret/!GetAtt UserPoolClientB.ClientSecret从客户端资源取回,随模板传入 Lambda 的属性(properties),全程不落明文 Outputs。

两个主机,切勿混淆

README 与 cognito.mjs 都强调同一件事:M2M 流程里有两个不同角色、不同域名的主机:

  • Issuer(签发方标识)https://cognito-idp.<region>.amazonaws.com/<poolId>——套件将其发布到 fixture 的oauthDiscovery.issuer字段,客户端读取受保护资源文档后被告知"去这里登录"。
  • Token endpoint(铸币端点)https://<domain>.auth.<region>.amazoncognito.com/oauth2/token——套件用它铸造 access token。

而 authorizer 实际引用的pool ARN 并不在此发布:套件在运行时由poolIdregion和调用方自己的账户 id 推导(见 mcp-auth.test.js:从 STSGetCallerIdentity取 partition 与 account id,拼出arn:<partition>:cognito-idp:<region>:<account>:userpool/<poolId>)。派生而非硬编码,避免了跨分区(partition)假设。

套件如何消费前置依赖:skip 与 fail 的边界

读取逻辑集中在 cognito.mjs 的readCognitoPrerequisite,它在**收集期(collection time)**执行一次,让套件在运行前就能决定是否可跑:

  • GetParametersByPathCommandWithDecryption: true、自动翻页)读取前缀下的全部参数;
  • 命中SKIPPABLE_ERROR_NAMES(一个白名单ParameterNotFound表示前缀不存在;CredentialsProviderError表示凭证链完全拿不到凭证)时才返回null→ 套件describe.skip
  • 其余任何失败都会向上抛出,让文件响亮失败。设计上刻意不用.catch兜底:被拒绝、限流、过期凭证、网络超时都是"本该成功的读取",静默跳过会让鉴权覆盖在什么都没跑的情况下被记为已覆盖,这是套件存在的意义所不允许的。

成功时返回的对象携带八个原始值,以及两个派生工具:issuertokenEndpoint,外加零参数的铸币器mintClientA()/mintClientB()。铸币走 mintToken:对 token endpoint POSTgrant_type=client_credentials+scope=mcp/invoke,HTTP Basic 认证,仅用全局fetch,因此请求形态可被单元测试用 stub 的 fetch 验证,无需真实网络与真实池(对应测试在 cognito.test.js)。

读取方(套件侧)在 mcp-auth.test.js 打印明确的跳过警告,指引开发者先部署前置模板。

部署:每个账户执行一次

模板所在目录持有template.yml,会让serverless deploy路由到框架的CloudFormation runner,由后者自行传递 IAM capabilities。从packages/sf-core/tests/integration/mcp-cognito-prerequisite/目录执行:

serverless deploy --stack mcp-integration-test-cognito --region us-east-1
  • 在已有该栈的账户上重复执行是安全的:会用上面命名的SsmWriterFunction替换写入函数并重写同样的八个参数。
  • 需要留意的一个小尾巴:早期版本创建过自动命名的日志组(/aws/lambda/<stack>-SsmWriterFunction-*),栈不会接管它,若在意可手动删除。
  • 成本几乎为零:Cognito 按 M2M 客户端收取的费用已于 2025 年 11 月取消;剩余成本为每 1000 次 token 请求 $0.00225,即便按每月 1000 次 CI 运行计也约$0.014/月;闲置池在约 0 MAU 下为 $0。

校验与排查

部署后可以用两类命令确认状态:

# 查看栈输出(非敏感发现值) serverless info --stack mcp-integration-test-cognito --region us-east-1 # 确认八个 SecureString 参数存在(--query 只列名称,不回显密钥值) aws ssm get-parameters-by-path --path /mcp-integration-test/cognito \ --with-decryption --region us-east-1 --query 'Parameters[].Name'

拆除与迁移

要把前置依赖迁到另一个账户(例如专门的 CI 账户),做法是在目标账户部署同一模板、在此账户拆除:

serverless remove --stack mcp-integration-test-cognito --region us-east-1

删除栈时,自定义资源会在 Delete 阶段清掉八个 SSM 参数,无需额外的手工清理步骤。

前置依赖之外的验证全景

虽然本前置只是"跑腿资源",但它支撑的测试断言才是理解其存在意义的关键(见 mcp-auth.test.js 的四个 describe 块):

  • cognito:垃圾 token 被网关以 401 拒绝,且从 CloudWatch 计数证明服务器函数从未被调用countInvocationsREPORT RequestId过滤整页扫描,拒绝与接受互为对照);用 Client A 的 token 跑完整 MCP 检查清单;再用 Client B 的 token 验证"同池同 scope 的任意客户端都被接受"。
  • custom / customRequest:TOKEN 与 REQUEST 两类 Lambda authorizer 形态,校验每次运行随机生成的共享密钥,钉死网关拒绝时的精确响应体(401 +{"message":"Unauthorized"}+x-amzn-errortype: UnauthorizedException)。
  • oauthDiscovery:MOCK 路由上的 RFC 9728 受保护资源文档的逐字节内容与 CORS 头,以及"文档恰好能被服务器路由拒绝的那个未认证客户端读取"这一关键性质;同时断言裸 execute-api 端点上的根探测返回 403(自定义域名映射到根路径时该探测才会解析)。
  • open:无 authorizer 的普通 MCP 往返,兼作流式传输与空 202 通知的回归门。

四个服务器在 serverless-auth.yml 中定义,指向同一份服务器模块(与fixture/的副本由 fixture-parity.test.js 强制逐字节一致),唯一变化的变量就是"谁被允许到达它"——这正是本前置依赖服务的验证目标。

延伸阅读

  • 前置依赖模板与说明:template.yml、README.md
  • 消费方套件与读取库:mcp-auth.test.js、cognito.mjs
  • fixture 配置:serverless-auth.yml、fixture-auth/README.md
  • 测试编排总览:TESTING.md(含该前置依赖在整个集成测试矩阵中的位置与运行方式)

【免费下载链接】serverless⚡ Serverless Framework – Effortlessly build apps that auto-scale, incur zero costs when idle, and require minimal maintenance using AWS Lambda and other managed cloud services.项目地址: https://gitcode.com/GitHub_Trending/se/serverless

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 7:59:19

从CDN加速到边缘智能:多语言工程的语法实践与框架改造

很多人把CDN理解成“一个缓存加速工具”&#xff0c;但真正在互联网工程里摸爬滚打过的同学都知道&#xff0c;CDN只是第一层&#xff0c;边缘智能才是在加速之上长出价值的那个点。而多语言场景&#xff0c;恰好是能把CDN加速、边缘路由、缓存策略、后端工程化串起来的最典型战…

作者头像 李华
网站建设 2026/9/10 7:58:40

蓝牙网关如何破解多人运动心率监测的接入难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 7:56:49

软考高项变更管理全解析:流程、CCB与实战应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 7:54:30

Zephyr 离线构建环境一次搭好

Zephyr 离线构建环境一次搭好 【免费下载链接】zephyr Primary Git Repository for the Zephyr Project. Zephyr is a new generation, scalable, optimized, secure RTOS for multiple hardware architectures. 项目地址: https://gitcode.com/GitHub_Trending/ze/zephyr …

作者头像 李华