ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

elasticsearch-js 前端代理示例详解:基于 Vercel Serverless Functions 的安全 Elasticsearch 访问方案

elasticsearch-js 前端代理示例详解:基于 Vercel Serverless Functions 的安全 Elasticsearch 访问方案
  • 后端
  • 搜索引擎

【免费下载链接】elasticsearch-js

Official Elasticsearch client library for Node.js

项目地址:https://gitcode.com/gh_mirrors/el/elasticsearch-js
点击查看免费下载

本指南完整解析 elasticsearch-js 官方仓库中 docs/examples/proxy 示例:在没有成熟后端的情况下,如何用一组轻量 Serverless 函数在「前端代码」与「Elasticsearch 集群」之间搭建安全代理。读完本文,你将掌握该示例的四个 API 端点设计、Api Key 的生成与Bearer令牌传递机制、Vercel 部署与 CORS 配置方法,以及read/write权限分离的最佳实践。

背景:为什么前端需要一个代理

浏览器直接访问 Elasticsearch 集群存在两个核心风险:一是会暴露集群的真实地址与凭据,二是恶意用户可以直接向集群投递任意查询 DSL——例如构造代价高昂的聚合查询来拖垮集群。该示例的思路是:在中间加一层极薄的 Serverless 代理,前端只与代理通信,代理持有集群凭据并以受限 Api Key 代表前端执行操作,从而把「谁能查」「能查到什么」的控制权收回到服务端。

需要特别强调的是,示例在 README.md 与每个源码文件顶部都标注了同一句警告:

IMPORTANT:This is not a production ready code and it is only for demonstration purposes, we make no guarantees on it's security and stability.

即该代码仅用于演示,官方不对其安全性与稳定性做任何担保,生产环境必须自行加固。

项目结构与四个端点

示例代码位于 docs/examples/proxy,目录结构如下:

docs/examples/proxy/ ├── api/ │ ├── autocomplete.js # 自动补全建议 │ ├── delete.js # 删除文档 │ ├── index.js # 索引/更新文档 │ └── search.js # 搜索 ├── utils/ │ ├── authorize.js # 请求授权逻辑 │ └── prepare-elasticsearch.js # 生成 Api Key 的脚本 ├── README.md ├── package.json # 依赖:@elastic/elasticsearch ^8.0.0 └── vercel.json # Vercel 平台配置(CORS 头等)

项目一共提供四个端点,且按权限分为两类:

端点功能所需权限对应源码
/api/search执行搜索readapi/search.js
/api/autocomplete自动补全建议readapi/autocomplete.js
/api/index索引或更新文档writeapi/index.js
/api/delete删除文档writeapi/delete.js

授权逻辑统一收敛在 utils/authorize.js 中,每个端点都需自行配置INDEX变量(源码中为占位符'<index-name>')。

依赖方面,package.json 仅声明了两个依赖:运行时依赖@elastic/elasticsearch(^8.0.0,即本仓库对应的官方 Node.js 客户端),以及 devDependencystandard(用于npm test的代码风格检查)。

配置 Elasticsearch:用 Cloud ID 生成受限 Api Key

准备阶段

  1. 在 Vercel 注册账号;
  2. 在 Elastic Cloud 创建一次 deployment(没有账号可通过 Elasticsearch Service 的 14 天免费试用创建);
  3. 复制部署生成的 Cloud Id 与管理员凭据。

修改prepare-elasticsearch.js

打开 utils/prepare-elasticsearch.js,填入四项配置:

// 你的 Cloud Id const cloudId = '' // 管理员用户名 const username = '' // 管理员密码 const password = '' // 需要访问的索引或索引模式 const indexNames = ['my-index-name-or-pattern'] const privileges = ['read']

脚本随后以管理员身份调用client.security.createApiKey创建 Api Key,并将 Api Key 绑定到限定indexNames与privileges的角色描述符上:

const result = await client.security.createApiKey({ name: 'elasticsearch-proxy', role_descriptors: { 'elasticsearch-proxy-users': { index: [{ names: indexNames, privileges }] } } }) return Buffer.from(`${result.id}:${result.api_key}`).toString('base64')

关键点:

  • 创建 Api Key 时通过role_descriptors把权限限定在指定索引或索引模式(names)与指定操作(privileges)上,这正是"最小权限"思想的落地:即便 Api Key 泄露,攻击者也只能操作配置范围内的工作;
  • 脚本输出的是一段id:api_key的 Base64 编码字符串,即 Elasticsearch 的ApiKey认证凭据格式;
  • privileges可按需调整,例如仅搜索场景用['read'],需要写入则加上['write'],索引级权限的完整清单可参考官方 security-privileges 文档。

从客户端实现看,src/client.ts 中cloud配置只需提供id字符串,构造器会按name:base64string格式解析 Cloud Id(src/client.ts),解码出https://<esId>.<cloudUrl>作为集群地址,并自动切换到CloudConnectionPool、默认开启压缩与 TLSv1.2(src/client.ts)。这意味着代理端的两个 Client 实例都无需手写集群 URL,只靠环境变量注入的 Cloud Id 即可连接。

配置 Vercel:本地调试、部署与环境变量

安装 Vercel CLI 后按以下三步走:

  1. vercel—— 引导初始化项目;
  2. vercel dev—— 本地起一个开发环境测试函数;
  3. vercel deploy—— 部署到线上。

此外必须为项目配置ELASTIC_CLOUD_ID环境变量,四个端点中每个Client实例都通过process.env.ELASTIC_CLOUD_ID读取它:

const client = new Client({ cloud: { id: process.env.ELASTIC_CLOUD_ID } })

也就是说,Api Key 由prepare-elasticsearch.js生成后从前端传入,而 Cloud Id 只存在于服务端环境变量中,前端永远接触不到集群的真实连接信息。

CORS 等附加配置写入 vercel.json,示例对/api/(.*)路径统一注入了跨域头:

{ "headers": [ { "source": "/api/(.*)", "headers": [ { "key": "Access-Control-Allow-Credentials", "value": "true" }, { "key": "Access-Control-Allow-Origin", "value": "*" }, { "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" }, { "key": "Access-Control-Allow-Headers", "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" } ] } ] }

注意:示例将Access-Control-Allow-Origin设为*,属于演示性质的宽松配置,生产环境应替换为具体的前端域名白名单。示例本身面向 Vercel 设计,README 也提示可以移植到其他函数托管平台(如 Google Cloud Functions)。

认证设计:Bearer 令牌与 Api Key 的流转

授权链路

Api Key 的完整流转路径是:

  1. 管理员运行prepare-elasticsearch.js生成受限 Api Key;
  2. 前端应用把 Api Key 放入请求的Authorization头,格式为Bearer <api-key>;
  3. 代理端点先调用 utils/authorize.js 校验并提取令牌;
  4. 端点再以ApiKey <token>头的形式把令牌透传给 Elasticsearch。

授权工具源码解析

utils/authorize.js 的实现非常简洁,核心逻辑如下:

module.exports = (req) => { const auth = req.headers.authorization if (typeof auth !== 'string') { return [{ error: 'Unauthorized', message: 'Missing authorization header', statusCode: 401 }, null] } const [type, token] = req.headers.authorization.split(' ') if (type !== 'Bearer') { return [{ error: 'Unauthorized', message: 'Bad authorization type', statusCode: 401 }, null] } if (token.length === 0) { return [{ error: 'Unauthorized', message: 'Bad authorization token', statusCode: 401 }, null] } return [null, token] }

它依次做三件事:检查Authorization头是否存在、认证类型是否为Bearer、令牌是否非空。任何一步不满足都返回401错误对象;通过后返回[null, token]供端点使用。返回值约定为元组[err, result],各端点据此分流:err非空则返回 401 JSON,否则继续业务逻辑。

两种典型使用场景

README 给出了两种与 Api Key 权限配套的使用方式:

  • 纯搜索场景(如搜索框):创建带read权限的 Api Key,直接存进前端应用,随请求经Authorization头传给代理执行搜索。因为只读,即便泄露也造不成数据破坏。
  • 需要写入数据的场景:更安全的做法是引入外部认证服务(如 Auth0、Magic Link),先完成用户身份认证,再为"已认证用户"签发read+write权限的 Api Key,绝不把这种 Api Key 存进前端。

两类 Api Key 的差异可以概括为:read密钥可以暴露给前端,write密钥必须由服务端掌控。

端点源码逐析

四个端点遵循完全一致的骨架:授权 → 参数校验 → 调用客户端 API(透传 Api Key)→ 统一错误处理。

/api/search:受限的搜索端点

api/search.js 校验req.body.text必须为字符串,随后执行:

const response = await client.search({ index: INDEX, query: { match: { field: req.body.text } } }, { headers: { Authorization: `ApiKey ${token}` } })

源码注释点明了设计意图:前端可以直接把 Elasticsearch 查询 DSL 原样发给代理,但这会让恶意用户通过构造昂贵查询来打爆集群;因此代理只接受text字段,在服务端固定拼装match查询——把"可查询的 DSL 面"收缩到最小。此外注释提示可配置 HTTP 缓存控制头(如stale-while-revalidate)来缓解查询压力。

/api/autocomplete:自动补全端点

api/autocomplete.js 从req.query.q读取关键词,并做两道校验:必须是字符串、长度至少为 3。随后基于 Elasticsearch 的 completion suggester 执行建议查询:

const response = await client.search({ index: INDEX, _source: ['id', 'url', 'name'], // 补全结果中展示的字段 size: 0, suggest: { suggestions: { prefix: req.query.q, completion: { field: 'suggest', size: 5 } } } }, { headers: { Authorization: `ApiKey ${token}` } })

要点:通过_source白名单只返回补全所需的少量字段;size: 0表示不返回普通命中文档;field: 'suggest'对应索引中类型为completion的字段。使用前提是目标索引已配置 completion 类型的 suggester 字段。

/api/index:写入端点

api/index.js 校验req.body必须是对象,然后以req.query.id作为文档 ID 调用client.index:

const response = await client.index({ index: INDEX, id: req.query.id, document: req.body }, { headers: { Authorization: `ApiKey ${token}` }, meta: true }) res.status(response.statusCode) res.json(response.body)

这里用了meta: true,让响应携带完整的元信息(含 HTTP 状态码),再把statusCode与响应体原样回给前端。由于需要写索引,调用该端点必须使用带write权限的 Api Key。

/api/delete:删除端点

api/delete.js 要求req.query.id非空,然后执行client.delete({ index: INDEX, id: req.query.id })并透传ApiKey头。与/api/index一样,它要求write权限,是四个端点中破坏性最强的一个——生产环境应进一步限制谁有权限调用它。

统一错误处理模式

四个端点共用同一套错误处理逻辑:

catch (err) { res.status(err.statusCode || 500) res.json({ error: err.name, message: err.message, statusCode: err.statusCode || 500 }) }

即优先透出 Elasticsearch 返回的 HTTP 状态码,未知错误统一回落为 500,并保持 JSON 错误结构一致(error/message/statusCode),方便前端统一解析。

安全与生产化注意事项

基于 README 的警告与源码实现,可以总结出以下加固方向(均为演示代码之外的工程实践):

  • Api Key 权限最小化:privileges只授予业务真正需要的操作(如仅read),names精确到索引或索引模式;
  • 写操作必须走强认证:write权限的 Api Key 绝不进前端,先经 Auth0/Magic Link 等外部身份服务完成用户认证后再签发;
  • 收紧 CORS:Access-Control-Allow-Origin从*改为前端真实域名;
  • 查询面收窄:仿照/api/search的做法,在代理层白名单化查询 DSL,而不是透传任意查询体;
  • 敏感信息不入前端:ELASTIC_CLOUD_ID只存在于服务端环境变量,前端只持有受限 Api Key。

小结

这个示例展示了 elasticsearch-js 在 Serverless 场景下的一种典型用法:用cloud.id免去手动拼装集群地址,用client.security.createApiKey生成按索引与权限收敛的 Api Key,用一层薄代理完成「前端 ↔ 代理 ↔ Elasticsearch」之间的凭据隔离与查询白名单化。虽然官方明确声明它不适合直接上生产,但它为"搜索框 + 受限写入"这类轻量需求提供了一套结构清晰、可立即本地运行验证的参考骨架。

示例代码采用 Apache 2.0 许可(见仓库根目录 LICENSE),可自由参考与改造。

  • 后端
  • 搜索引擎

【免费下载链接】elasticsearch-js

Official Elasticsearch client library for Node.js

项目地址:https://gitcode.com/gh_mirrors/el/elasticsearch-js
点击查看免费下载
上一篇:抖音批量下载:贴一个主页链接,把上百条作品一次存进本地
下一篇:30秒搞定 JetBrains 试用期重置:ide-eval-resetter 上手指南

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

RELATED READING

延伸阅读

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