- 后端
- 搜索引擎
【免费下载链接】elasticsearch-js
Official Elasticsearch client library for Node.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 | 执行搜索 | read | api/search.js |
/api/autocomplete | 自动补全建议 | read | api/autocomplete.js |
/api/index | 索引或更新文档 | write | api/index.js |
/api/delete | 删除文档 | write | api/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
准备阶段
- 在 Vercel 注册账号;
- 在 Elastic Cloud 创建一次 deployment(没有账号可通过 Elasticsearch Service 的 14 天免费试用创建);
- 复制部署生成的 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 后按以下三步走:
vercel—— 引导初始化项目;vercel dev—— 本地起一个开发环境测试函数;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 的完整流转路径是:
- 管理员运行
prepare-elasticsearch.js生成受限 Api Key; - 前端应用把 Api Key 放入请求的
Authorization头,格式为Bearer <api-key>; - 代理端点先调用 utils/authorize.js 校验并提取令牌;
- 端点再以
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
相关推荐
Vercel Python Serverless Functions 实战:基于 Hello World 示例掌握 Python Runtime 部署
Vercel Python Serverless Functions 实战:基于 Hello World 示例掌握 Python Runtime 部署 导读 本
示例工程前端后端elasticsearch-dump与Azure Functions集成:serverless数据迁移方案
elasticsearch dump与Azure Functions集成:serverless数据迁移方案 数据迁移仍在消耗你70%的运维时间?当需要在Elas
后端数据工程搜索引擎在 Next.js 中接入 Elasticsearch 搜索后端:with-elasticsearch 示例应用全解
在 Next.js 中接入 Elasticsearch 搜索后端:with elasticsearch 示例应用全解 本指南以 Next.js 官方仓库中的 e
前端后端Web框架SSR前端构建
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考