十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Graphile Crystal 持久化操作指南:使用 @grafserv/persisted 为 Grafserv 打造查询白名单

Graphile Crystal 持久化操作指南:使用 @grafserv/persisted 为 Grafserv 打造查询白名单 后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载本指南围绕 Graphile Crystal Monorepo 中的 grafast/grafserv-persisted 包展开系统讲解如何为基于 Grafserv 的 GraphQL 服务PostGraphile 或其他启用持久化操作Persisted Operations也称 persisted queries / query allowlist / persisted documents以加固 GraphQL API 安全性并追踪字段使用情况。读完本文你将掌握插件的安装配置、五个核心选项的取舍、底层 middleware 拦截原理以及 Relay、GraphQL Code Generator、Apollo Client 三套主流的持久化操作生成与接入方案。为什么需要持久化操作常规 GraphQL 请求会把完整的查询文档query随请求发送到服务端服务端需要对该文档做解析、校验、执行。对于只允许第一方客户端访问 GraphQL schema 的服务PostGraphile 或其他这会带来两方面问题攻击面任何拿到端点的人都可能提交任意查询包括深度嵌套、高开销的恶意文档形成拒绝服务DoS风险可观测性无法轻易追踪客户端到底使用了哪些字段难以支撑 schema 演进与裁剪决策。持久化操作的思路是把允许执行的 GraphQL 操作文档提前登记到服务端客户端请求时只携带一个哈希 IDhash / documentId服务端据此映射回原始文档再执行。请求体不再包含任意查询文本攻击者无从注入未登记的操作。grafserv/persisted是 Graphile 为此给出的官方解决方案它适用于 Grafserv 处理的标准 GET 与 POST 请求也适用于 websocket 连接对查询queries、变更mutations、订阅subscriptions三种操作类型全部生效。安装在项目内安装grafserv/persistedyarn add grafserv/persisted # 或 npm install --save grafserv/persisted从 package.json 可以看到该包以grafast、grafserv、graphile-config、graphql^16.9.0为 peer 依赖运行环境要求 Node.js 22内部依赖graphile/lru用于缓存 getter与tslib。快速上手在 graphile.config 中启用插件grafserv/persisted导出名为PersistedPlugin的插件同时提供 default 导出二者等价。将其加入graphile.config.ts或你使用的等价配置文件的plugins列表即可import graphile-config; import PersistedPlugin from grafserv/persisted; const preset: GraphileConfig.Preset { plugins: [PersistedPlugin], grafserv: { /* add configuration options here, e.g. */ persistedOperationsDirectory: ${process.cwd()}/.persisted_operations, }, }; export default preset;插件通过全局命名空间扩展见 src/index.ts 中的declare global把新选项注册进GraphileConfig.GrafservOptions因此配置项会获得完整的 TypeScript 类型提示与文档注释。配置选项详解插件向 Grafserv 选项注入以下可选配置项全部可选但要让插件真正发挥作用应指定persistedOperationsDirectory、persistedOperations、persistedOperationsGetter三选一hashFromPayload如何从请求中提取哈希hashFromPayload?(request: ParsedGraphQLBody): string | undefined;该函数接收一个 GraphQL 请求对象常规形态为{query: string, variables?: any, operationName?: string, extensions?: any}但持久化操作请求通常没有query属性从中提取用于定位持久化操作的哈希。例如 Apollo Client 可提取request?.extensions?.persistedQuery?.sha256HashRelay 可提取request?.documentId。若不提供插件使用内置的defaultHashFromPayload见 src/index.ts按以下优先级兜底兼容payload?.extensions?.persistedQuery?.sha256Hash—— Apollo Client 协议payload?.documentId—— Relay 协议payload?.id—— 非标准字段。persistedOperationsDirectory从目录读取hash.graphqlpersistedOperationsDirectory?: string;指定一个文件夹其中每个文件命名为hash.graphql如abc123def.graphql文件内容为对应的 GraphQL 操作文档。使用该方式时首次读取 内存缓存某个哈希第一次被请求时才发生文件系统读取之后结果被缓存后续请求不再触碰文件系统周期性目录扫描 DoS 防护插件会周期性扫描目录以发现新文件对上次扫描中不存在的哈希的请求会被直接拒绝避免攻击者用不存在的哈希反复触发文件系统 IO 造成拒绝服务。从源码看makeGetterForDirectorysrc/index.ts使用fs.promises.readdir维护文件名清单对哈希做/^[a-zA-Z0-9_-]$/格式校验防路径穿越并通过operationFromHashMap 缓存每个哈希的读取 Promise / 字符串结果。目录 getter 还会按目录与扫描间隔去重缓存directoryGetterByDirectory确保同一目录只建立一次扫描与 watcher。persistedOperationsDirectoryScanInterval目录扫描间隔persistedOperationsDirectoryScanInterval?: number | watch;决定多久扫描一次持久化操作目录以检查新文件数字毫秒间隔默认-1禁用周期扫描字符串watch改用fs.watch监听目录变更实验性。源码在scanInterval watch时通过fsp.watch(directory, { signal, recursive: false })建立监听文件事件触发重新扫描数字模式则采用“上一次扫描完成后才安排下一次扫描”的setTimeout策略避免扫描时长与间隔叠加错乱。abortController的存在暗示未来会支持通过 AbortController 停止监听当前源码留有 TODO。persistedOperations内存哈希表persistedOperations?: { [hash: string]: string };一个字符串到字符串的键值对象键为哈希、值为操作文档字符串。适用于操作集合固定、可随配置直接嵌入的场景。源码中它被转换为(key: string) cache[key]形式的 getterpersistedOperationGetterForCache完全不涉及文件系统。persistedOperationsGetter按需加载 高性能要求persistedOperationsGetter?: PersistedOperationGetter;当已知的持久化操作可能随时间变化、或希望按需加载时可以提供一个(hash: string) PromiseOrDirectstring函数。该函数处于性能关键路径每次请求都会调用官方强烈建议内部使用缓存加速后续相同哈希的查询。类型定义见 interfaces.ts。allowUnpersistedOperation按条件放行未登记操作allowUnpersistedOperation?: | boolean | ((event: ProcessGraphQLRequestBodyEvent) boolean);有时需要允许任意操作例如开发环境用 GraphiQL 调试、生产环境允许管理员发任意请求而应用用户与普通用户仍强制使用持久化操作。此选项既可以是布尔值也可以是接收ProcessGraphQLRequestBodyEvent返回布尔值的函数用于决定何时绕过持久化操作限制。注意该函数绝不能抛异常。官方示例allowUnpersistedOperation(event) { return process.env.NODE_ENV development event.request?.getHeader(referer)?.endsWith(/graphiql); }选项互斥与缺失校验源码getterFromOptionsCoresrc/index.ts会检查persistedOperationsGetter、persistedOperationsDirectory、persistedOperations三个选项同时指定多个会抛错提示“at most one of these operations can be specified”一个都没指定则抛出“Server misconfiguration issue”错误拒绝启动。另外插件通过getterFromOptionsCachegraphile/lru容量 100缓存按 options 对象解析出的 getter避免重复构造。底层原理middleware 如何改写请求体PersistedPlugin的本质是一个 Grafserv middleware见 src/index.ts注册在grafserv.middleware.processGraphQLRequestBody钩子上。Grafserv 在处理 HTTP GraphQL 请求时会先解析请求体再运行该 middleware 链见 middleware/graphql.tswebsocket 订阅场景则通过onSubscribe流程执行同一钩子utils.ts。完整流程为请求体HTTP JSON/查询串或 websocket payload先被解析为ParsedGraphQLBody保留id、documentId、query、operationName、variables、extensions等字段utils.ts插件计算shouldAllowUnpersistedOperation并调用persistedOperationFromPayloadsrc/index.ts用hashFromPayload或默认实现从 payload 提取哈希找不到哈希时若allowUnpersistedOperation为真且请求携带query则原样放行该 query否则返回 null找到哈希时通过选定的 getter 取得操作文档字符串拿到文档字符串后就地覆写body.querybody.query q若解析失败或哈希对应的操作不存在抛出SafeError返回 HTTP 400「Persisted operations are enabled on this server, please provide an approved document id.」后续执行链路与普通请求完全一致——插件对上层透明parseAndValidate(query)、执行、订阅照常进行middleware/graphql.ts。值得注意的细节persistedOperationFromPayload自身永不抛异常内部 catch 后返回 null错误交由上层统一处理并且哈希提取失败时会在服务端console.error打印 payload 与错误信息便于排查客户端接入问题。生成持久化操作服务端登记了哈希映射客户端还得能生成哈希并只发送哈希。官方推荐两种工具链。Relay内置支持Relay 原生支持持久化操作。在relay-compiler正常参数后追加relay-compiler --persist-output ./path/to/server.json ...生成的server.json是哈希到操作文档的映射。之后用下面给出的addToPersistedOperations.js脚本把这份 JSON 拆分为每个查询一个文件交给persistedOperationsDirectory指向的目录。同时在网络层fetchQuery把query: operation.text改为documentId: operation.id即客户端只发送文档 ID。GraphQL Code Generator推荐除 Relay 外官方推荐在构建客户端时用 graphql-code-generator 的graphql-codegen-persisted-query-ids插件README 标注测试于 v0.1.2生成。一个典型配置schema: schema.graphql documents: src/**/*.graphql hooks: afterAllFileWrite: - node addToPersistedOperations.js generates: client.json: plugins: - graphql-codegen-persisted-query-ids: output: client algorithm: sha256 server.json: plugins: - graphql-codegen-persisted-query-ids: output: server algorithm: sha256output: client生成客户端用的哈希清单配合 Apollo 等使用output: server生成服务端用的哈希→文档映射。同时用afterAllFileWrite钩子在每次构建后自动执行拆分脚本便于版本控制// addToPersistedOperations.js const map require(./server.json); const { promises: fsp } require(fs); async function main() { await Promise.all( Object.entries(map).map(([hash, query]) fsp.writeFile( ${__dirname}/.persisted_operations/${hash}.graphql, query, ), ), ); } main().catch((e) { console.error(e); process.exit(1); });脚本把server.json的每个条目写成.persisted_operations/hash.graphql文件该目录即通过persistedOperationsDirectory传入服务端配置见前文快速上手示例。Apollo Client配合apollo-link-persisted-queries可以让 Apollo Client 发送由 graphql-codegen 预生成的哈希import { createPersistedQueryLink } from apollo-link-persisted-queries; import { usePregeneratedHashes as withPregeneratedHashes } from graphql-codegen-persisted-query-ids/lib/apollo; import { hashes } from ./path/to/client.json; const persistedLink createPersistedQueryLink({ useGETForHashedQueries: false, generateHash: withPregeneratedHashes(hashes), disable: () false, }); // ... const client new ApolloClient({ link: ApolloLink.from([persistedLink, httpLink]), // ... });客户端使用预生成的哈希请求以extensions.persistedQuery.sha256Hash形式携带哈希正好命中插件内置的默认哈希提取逻辑。安全与工程实践建议生产环境默认开启任何只打算服务第一方客户端的 GraphQL 服务都应启用持久化操作。它能压缩攻击面服务端只执行已登记文档、辅助字段使用追踪、为 schema 演进提供依据拒绝服务缓解优先使用persistedOperationsDirectory 目录扫描配合对未知哈希的即时拒绝persistedOperations全量内存表在操作集合稳定时最简单高效操作频繁变动时使用带缓存的persistedOperationsGetter并自行实现缓存该函数处于性能关键路径开发与生产分离用allowUnpersistedOperation函数按环境放行例如仅当NODE_ENV development且Referer指向 GraphiQL 时才允许未登记操作注意该函数必须同步返回、绝不能抛异常哈希来源统一客户端生成的哈希算法如sha256需与服务端映射一致Relay 使用documentId、Apollo 使用extensions.persistedQuery.sha256Hash均被插件默认实现覆盖自定义协议则用hashFromPayload适配校验哈希格式服务端对哈希执行/^[a-zA-Z0-9_-]$/校验客户端应避免使用含路径分隔符等特殊字符的哈希值。参考插件实现与配置类型grafast/grafserv-persisted/src/index.ts、grafast/grafserv-persisted/src/interfaces.tsGrafserv middleware 调用链grafast/grafserv/src/middleware/graphql.ts、grafast/grafserv/src/utils.ts包元数据与依赖要求grafast/grafserv-persisted/package.json变更记录grafast/grafserv-persisted/CHANGELOG.md赞分享后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载相关推荐grafserv/persisted 解析为 Grafserv 与 PostGraphile 构建持久化操作Persisted Operations支持grafserv/persisted 解析为 Grafserv 与 PostGraphile 构建持久化操作Persisted Operations支持后端API网关Grafserv 从 Alpha 到 1.0Graphile Crystal 中 GraphQL 服务器适配层的演进之路Grafserv 从 Alpha 到 1.0Graphile Crystal 中 GraphQL 服务器适配层的演进之路 Grafserv 是 Graphil后端API网关Grafserv 错误掩码Error Masking实战指南在 Graphile Crystal 中安全地暴露与隔离 GraphQL 错误Grafserv 错误掩码Error Masking实战指南在 Graphile Crystal 中安全地暴露与隔离 GraphQL 错误 Grafser后端API网关创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表