ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Serverless Framework 实战:用 Lambda 授权器保护 WebSocket 连接(aws-node-websockets-authorizers 示例全解析)

Serverless Framework 实战:用 Lambda 授权器保护 WebSocket 连接(aws-node-websockets-authorizers 示例全解析)
  • 示例工程

【免费下载链接】examples

Serverless Examples – A collection of boilerplates and examples of serverless architectures built with the Serverless Framework on AWS Lambda, Microsoft Azure, Google Cloud Functions, and more.

项目地址:https://gitcode.com/gh_mirrors/exa/examples
点击查看免费下载

本文基于仓库中的 aws-node-websockets-authorizers 示例,完整演示如何在 Serverless Framework 中为 AWS API Gateway WebSocket API 的$connect路由挂载 Lambda 授权器(Authorizer),实现"无凭据连接被拒绝、携带合法请求头才能建立连接"的鉴权闭环。读完本文,你将掌握identitySource身份来源表达式的写法、授权器返回 IAM Policy 的实现要点,以及使用wscat完成端到端验证的完整流程。

示例概览与核心目标

WebSocket API 与普通 HTTP API 有一个关键差异:客户端通过 HTTP 握手发起升级请求,之后便维持一条长连接用于双向消息推送。因此,鉴权动作必须发生在握手(连接建立)阶段,而不是每条消息上。本示例的目标非常清晰:

  1. 客户端不带任何凭据连接 WebSocket 时,连接被直接拒绝;
  2. 客户端在握手请求中携带Auth: secret请求头时,连接成功建立;
  3. 建立连接后,服务端通过$default路由将客户端发来的消息原样回显,形成最简单的"聊天"能力。

示例共包含三个核心文件:入口说明文档 README.md、路由与授权器声明文件 serverless.yml、以及全部业务逻辑所在的 handler.js。

项目结构与关键文件

文件作用
serverless.yml声明服务、Provider、三个 Lambda 函数及 WebSocket 路由事件
handler.js实现connect、disconnect、default、auth四个导出函数
package.jsonNode.js ESM 工程声明,唯一运行时依赖为@aws-sdk/client-apigatewaymanagementapi

package.json中值得注意的细节是"type": "module",这意味着handler.js采用 ES Module 语法(import/export),与 Serverless Framework v4 的默认运行时代保持一致;其依赖@aws-sdk/client-apigatewaymanagementapi正是$default路由回显消息时调用的 AWS SDK 客户端,用于向已建立的连接推送数据。

serverless.yml:授权器与路由配置详解

先看完整的服务定义(来自 serverless.yml):

service: websocket-authorizer-example frameworkVersion: '4' provider: name: aws stage: dev runtime: nodejs24.x architecture: arm64 functions: connect: handler: handler.connect events: - websocket: route: $connect # authorizers are only for connect routes authorizer: name: auth identitySource: - 'route.request.header.Auth' default: handler: handler.default events: - websocket: route: $default auth: handler: handler.auth

服务与 Provider 配置

  • service: websocket-authorizer-example定义服务名,它将作为 CloudFormation 堆栈与资源命名的基础。
  • frameworkVersion: '4'声明本示例面向 Serverless Framework v4(README 元数据中framework: v4与此一致)。
  • provider块固定了运行环境:aws平台、dev阶段、nodejs24.x运行时,并选用arm64架构(Graviton),这也是当前 AWS Lambda 性价比更高的默认选择。

connect 路由:授权器的唯一挂载点

connect: handler: handler.connect events: - websocket: route: $connect authorizer: name: auth identitySource: - 'route.request.header.Auth'

$connect是 API Gateway WebSocket 的三个内置路由之一,在客户端握手时触发,是鉴权的最佳时机。配置中嵌套的authorizer对象完成了两件关键事:

  • name: auth:将本服务内名为auth的函数(即下方functions.auth)指定为授权器;
  • identitySource:声明授权器需要从请求的哪个位置提取身份凭据。

配置文件中的注释 "authorizers are only for connect routes" 直接点明了设计约束:在 API Gateway WebSocket 中,授权器只能挂载在$connect路由上。这是因为鉴权必须在握手阶段一次性完成,$connect通过后才建立连接;后续$default、$disconnect等路由的通信都建立在已通过鉴权的连接之上。

identitySource:身份来源表达式

identitySource列表中的'route.request.header.Auth'是 API Gateway 的上下文表达式语法,含义为"从握手请求的 HTTP 头中提取名为Auth的头"。它等价于 REST API 中常见的method.request.header.*写法(本仓库 mcp/oauth-authorizer 示例即使用method.request.header.Authorization提取Authorization头),区别在于 WebSocket 事件统一用route.request.*前缀描述握手阶段的请求上下文。

当授权器需要从多个位置提取凭据时,可在identitySource列表中追加多条表达式,例如同时提取Auth头与查询字符串参数。凡是列表中出现但请求中缺失的来源,API Gateway 会直接拒绝握手而不必调用授权器函数。

$default 路由与 auth 函数声明

default: handler: handler.default events: - websocket: route: $default auth: handler: handler.auth

$default是兜底路由:客户端在已建立的连接上发送的任何、未匹配到其他自定义路由的消息都会落到这里,本示例用它实现消息回显(聊天)。而auth函数不需要events声明,因为它的入口由connect路由的authorizer.name引用——Serverless Framework 会据此自动创建授权器资源并绑定到 API Gateway。

授权器 Lambda 的源码实现

授权器函数位于 handler.js,是理解整个示例的关键:

export const auth = async (event) => { // return policy statement that allows to invoke the connect function. // in a real world application, you'd verify that the header in the event // object actually corresponds to a user, and return an appropriate statement accordingly return { principalId: 'user', policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke', Effect: 'Allow', Resource: event.methodArn, }, ], }, }; };

返回 IAM Policy 而非业务响应

与普通 Lambda 返回statusCode/body不同,授权器函数返回的是一个IAM Policy 文档(Policy Document),API Gateway 依据其中的Allow/Deny决定是否放行本次握手。源码中的三个字段逐一说明:

  • principalId:身份主体标识,本示例固定为字符串'user',用于在 CloudWatch 日志中标识本次鉴权的调用者;真实场景中通常填入用户 ID、sub或client_id。
  • policyDocument:IAM 策略文档本体,Version: '2012-10-17'是 IAM 策略的固定版本号;
  • Statement数组:Action: 'execute-api:Invoke'表示授予"调用 API Gateway 执行 API"的权限,Effect: 'Allow'明确放行,而Resource: event.methodArn则把授权范围精确限定在本次请求对应的资源 ARN 上——即本次$connect握手调用本身。

这段代码体现了授权器设计的核心思想:授权器不做业务响应,只做"准入裁决"。event.methodArn由 API Gateway 自动注入,指向当前被请求的 WebSocket 路由 ARN,开发者无需也无法预先知道它的具体值,直接透传即可。

真实场景中的校验逻辑

源码注释明确提示了生产环境的正确做法:

in a real world application, you'd verify that the header in the event object actually corresponds to a user, and return an appropriate statement accordingly

也就是说,本示例为了演示而固定返回Allow。真实场景中,授权器应当:

  1. 从event中读取identitySource指定的凭据(如event.headers.Auth或 TOKEN 授权器形态下的event.authorizationToken);
  2. 校验其有效性——例如验签 JWT、查数据库、比对 HMAC 等;
  3. 依据校验结果返回Allow或Deny策略。

关于拒绝形态,本仓库 mcp/oauth-authorizer 的文档给出了可供参考的实战经验:抛异常让授权器失败会得到 API Gateway 的401响应,而显式返回Deny策略则得到403;无论哪种形态,被拒绝的请求都不会触发$connect函数的执行——这正是授权器"前置保护"的价值所在。

连接生命周期处理器

connect与disconnect两个函数负责维护连接生命周期(handler.js):

export const connect = async () => ({ statusCode: 200, body: 'Connected.', }); export const disconnect = async () => ({ statusCode: 200, body: 'Disconnected.', });
  • connect:在握手通过鉴权后执行,返回200表示连接建立成功。若返回其他状态码(如401、403),API Gateway 会终止握手——这是鉴权"第二道防线";
  • disconnect:在连接关闭时触发,可用于清理会话状态、解绑资源等收尾工作。

而$default路由的处理器展示了如何向连接主动推送数据(handler.js):

export const defaultHandler = async (event) => { // default function that just echos back the data to the client const client = new ApiGatewayManagementApiClient({ endpoint: `https://${event.requestContext.domainName}/${event.requestContext.stage}`, }); await client.send( new PostToConnectionCommand({ ConnectionId: event.requestContext.connectionId, Data: `default route received: ${event.body}`, }) ); return { statusCode: 200, body: 'Sent.', }; };

这里有两个值得记住的实现细节:

  1. 回调端点是动态拼接的:ApiGatewayManagementApiClient的endpoint由event.requestContext.domainName与event.requestContext.stage拼出https://{domainName}/{stage},这样才能精确定位当前 API 部署的网关地址;
  2. 回显依赖connectionId:向客户端推送消息必须携带event.requestContext.connectionId——该 ID 是连接的唯一标识,由 API Gateway 在握手阶段分配并注入事件上下文。

handler.js末尾的export { defaultHandler as default }是 ES Module 的默认导出写法,使serverless.yml中handler.default可以直接引用该函数。

端到端验证:从拒绝到放行

按 README.md 的操作步骤,可以完整走通整个鉴权闭环。

第一步:部署服务

在aws-node-websockets-authorizers目录下执行:

npm install serverless deploy

部署完成后,Serverless Framework 会输出 WebSocket 服务的wss端点地址(形如wss://xxxxxxxxxx.execute-api.<region>.amazonaws.com/dev)。注意该地址是wss://协议,而非 HTTP API 常见的https://。

第二步:无凭据连接——应当失败

使用wscat(Node.js 生态最常用的 WebSocket 命令行客户端)发起连接:

wscat -c <wss-url>

由于connect路由配置了授权器且identitySource要求提供Auth头,而本次请求没有携带任何凭据,连接应当失败——这正是授权器生效的证明:请求在到达$connect函数之前就被 API Gateway 拦截。

第三步:携带 Auth 头重连——应当成功

wscat -c <wss-url> -H Auth:secret

-H参数向握手请求附加自定义头Auth: secret。这一次identitySource提取到了身份来源,授权器函数被调用并返回Allow策略,$connect执行并返回200,连接成功建立。

第四步:自我聊天

连接建立后,在wscat交互终端中输入任意文本并回车,消息会经由$default路由回显为default route received: <你输入的内容>——即 README 中所说的 "Feel free to chat with yourself"。这一行为正是 handler.js 中PostToConnectionCommand回写逻辑的运行结果。

授权器工作原理与安全要点

将整个示例串联起来,一次带鉴权的 WebSocket 握手完整经过如下环节:

  1. 客户端发起wss握手,携带Auth请求头;
  2. API Gateway 匹配$connect路由,读取identitySource提取凭据;
  3. 凭据存在 → 调用auth授权器函数 → 返回 IAM Policy;
  4. Allow→ 执行connect函数 → 返回200→ 连接建立;
  5. 后续消息经$default路由处理,服务端用connectionId回显。

由此可提炼出本示例的安全设计要点:

  • 鉴权前置:凭据校验发生在任何业务函数执行之前,被拒绝的握手不会消耗connect函数的调用成本;
  • 身份来源可配置:identitySource决定凭据从哪提取(本示例是Auth头),可按业务需要扩展为Authorization头、查询参数等;
  • 最小授权:Resource: event.methodArn将策略范围精确限定到本次调用,避免授权器误放行其他路由。

如果希望将本示例的鉴权思路从"演示固定放行"升级为"真实校验用户身份",可以参考本仓库 mcp/oauth-authorizer 示例中基于 JWKS 验签、并区分Allow缓存与Deny/异常拒绝行为的完整实现,其resultTtlInSeconds缓存调优、TOKEN 形态授权器只接收event.authorizationToken等经验,同样适用于 WebSocket 授权器的生产化改造。

  • 示例工程

【免费下载链接】examples

Serverless Examples – A collection of boilerplates and examples of serverless architectures built with the Serverless Framework on AWS Lambda, Microsoft Azure, Google Cloud Functions, and more.

项目地址:https://gitcode.com/gh_mirrors/exa/examples
点击查看免费下载
上一篇:gsd-core PR 3577 Docker 回归修复解读:Intel 命令迁移、阶段编号精确匹配与深度感知删除的源码实现
下一篇:Desloppify Scorecard徽章完全指南:为你的GitHub仓库生成代码质量评分卡

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进