首页/新闻资讯/正文详情

TypeGraphQL 查询复杂度限制实战:用 graphql-query-complexity 为 Schema 字段定义成本并防 DDoS

发布时间:2026/9/26 2:14:32 来源:云帆数科 栏目:资讯中心
TypeGraphQL 查询复杂度限制实战:用 graphql-query-complexity 为 Schema 字段定义成本并防 DDoS
后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载本指南讲解 TypeGraphQL 的 Query Complexity查询复杂度能力你可以在Field、FieldResolver、Mutation、Subscription装饰器上为每个字段声明成本再借助graphql-query-complexity在任意 GraphQL 服务器中分析查询 AST、估算单次操作的总体复杂度从而拒绝超阈值的高开销查询保护服务器免受资源耗尽与 DDoS 攻击。读完本文你将掌握字段复杂度的两种定义方式数字与估算函数、TypeGraphQL 内部如何把复杂度写入 schema以及在 Apollo Server 中落地校验插件的完整流程。为什么要限制查询复杂度一个看似普通的 GraphQL 查询可能给服务器带来巨大负担。例如客户端请求一个列表字段而每个列表项又带若干个嵌套子字段一次操作就可能触发成千上万次数据库查询攻击者可以借此发起 DDoS 攻击。TypeGraphQL 官方文档docs/complexity.md明确指出单条 GraphQL 查询可能产生巨大的服务器工作量如数千次数据库操作并被利用来发动 DDoS 攻击因此需要跟踪并限制每次 GraphQL 操作所能做的事情。与其依赖超时或请求频率等外围手段更精准的做法是基于成本分析cost analysis先为每个字段定义成本complexity cost再分析查询的 AST抽象语法树估算整条查询的总成本。TypeGraphQL 将成本分析交给graphql-query-complexity库完成你只需要做两件事在 TypeGraphQL 装饰器中定义各字段的复杂度在所使用的任意 GraphQL 服务器中接入graphql-query-complexity。第一步在装饰器中为字段定义复杂度TypeGraphQL 允许把complexity作为选项传给字段/查询/变更/订阅的装饰器。复杂度可以是一个数字也可以是一个估算函数。ObjectType() class MyObject { Field({ complexity: 2 }) publicField: string; Field({ complexity: ({ args, childComplexity }) childComplexity 1 }) complexField: string; }数字形式Field({ complexity: 2 })表示该字段的成本恒为 2。函数形式({ args, childComplexity }) childComplexity 1函数的入参由graphql-query-complexity提供childComplexity子节点嵌套字段的复杂度之和args该字段查询/变更接收的参数可用来按参数动态计算成本。支持复杂度选项的装饰器官方文档说明complexity可以作为选项传给以下任意装饰器装饰器说明Field对象类型字段FieldResolver字段解析器Mutation变更操作Subscription订阅操作默认值与优先级规则可省略如果字段的复杂度值就是 1可以完全省略complexity选项simpleEstimator的默认行为也与此一致见下文。优先级对同一个属性如果FieldResolver与Field都定义了复杂度那么FieldResolver上传递的复杂度优先。这一点在示例代码中也有明确体现见 examples/query-complexity/recipe.resolver.ts 中的注释 Complexity in field resolver overrides complexity of equivalent field type。源码视角复杂度如何被收集并写入 schema从 TypeGraphQL 源码可以确认complexity选项的完整流转链路类型定义src/typings/Complexity.ts定义了export type Complexity ComplexityEstimator | number;即复杂度本质上就是graphql-query-complexity的估算器类型或一个数字。元数据收集在 src/decorators/Field.ts 中options.complexity同时被写入类字段元数据collectClassFieldMetadata和内部字段解析器元数据collectFieldResolverMetadatasrc/decorators/FieldResolver.ts 同样收集该选项。写入 GraphQL 字段配置在 src/schema/schema-generator.ts 中生成对象类型字段时会把complexity: field.complexity放入字段配置的extensions对象extensions: { complexity: field.complexity, ...field.extensions, ...fieldResolverMetadata?.extensions, },这正是graphql-query-complexity的fieldExtensionsEstimator()能够读取到各字段复杂度值的原因——TypeGraphQL 在构建 schema 时把复杂度作为字段扩展extensions挂在 GraphQL 字段配置上估算器在运行时解析这些扩展即可。接口类型字段与查询/变更/订阅处理器的生成逻辑同文件约 L478-L481、L668-L672也采用相同的extensions.complexity写入方式。第二步在服务器中集成 graphql-query-complexity定义好字段复杂度后下一步是把graphql-query-complexity集成进你实际使用的 GraphQL 服务器。该库支持多种服务器接入方式官方文档提到可参照express-graphql的用法TypeGraphQL 官方示例使用 Apollo Server。方案一Apollo Server 插件推荐与官方示例一致在 examples/query-complexity/index.ts 中通过 Apollo Server 的plugins机制在每个请求解析操作时调用getComplexity计算复杂度并对比阈值MAX_COMPLEXITY超限即抛错拒绝查询import { ApolloServer } from apollo/server; import { startStandaloneServer } from apollo/server/standalone; import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from graphql-query-complexity; import { buildSchema } from type-graphql; import { RecipeResolver } from ./recipe.resolver; // Maximum allowed complexity const MAX_COMPLEXITY 20; async function bootstrap() { // Build TypeGraphQL executable schema const schema await buildSchema({ resolvers: [RecipeResolver], emitSchemaFile: path.resolve(__dirname, schema.graphql), }); // Create GraphQL server const server new ApolloServer({ schema, // Create a plugin to allow query complexity calculation for every request plugins: [ { requestDidStart: async () ({ async didResolveOperation({ request, document }) { const complexity getComplexity({ // GraphQL schema schema, // To calculate query complexity properly, // check only the requested operation, // not the whole document that may contain multiple operations operationName: request.operationName, // GraphQL query document query: document, // GraphQL query variables variables: request.variables, // Add any number of estimators. The estimators are invoked in order, // the first numeric value returned by an estimator is used as the field complexity. // If no estimator returns a value, an exception is raised estimators: [ // Using fieldExtensionsEstimator is mandatory to make it work with type-graphql fieldExtensionsEstimator(), // This will assign each field a complexity of 1 // if no other estimator returned a value simpleEstimator({ defaultComplexity: 1 }), ], }); // React to the calculated complexity, // like compare it with max and throw error when the threshold is reached if (complexity MAX_COMPLEXITY) { throw new Error( Sorry, too complicated query! ${complexity} exceeded the maximum allowed complexity of ${MAX_COMPLEXITY}, ); } console.log(Used query complexity points:, complexity); }, }), }, ], }); // Start server const { url } await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(GraphQL server ready at ${url}); } bootstrap().catch(console.error);getComplexity关键参数说明参数作用schema由buildSchema构建出的可执行 GraphQL schema估算器需据此遍历字段operationName只计算文档中被请求的那一个操作避免一次文档含多个操作时误算queryGraphQL 查询文档variables查询变量。变量在graphql-js的 visitor 中不可直接获取因此需显式传入estimators估算器数组按顺序调用第一个返回数值的估算器决定该字段的复杂度若所有估算器都不返回值则抛出异常estimators 的配置要点fieldExtensionsEstimator()是必选项它是 TypeGraphQL 与graphql-query-complexity协作的桥梁——只有通过它TypeGraphQL 写入字段extensions.complexity的复杂度值才会被读取。官方文档与示例代码均强调 Using fieldExtensionsEstimator is mandatory to make it work with type-graphql。simpleEstimator({ defaultComplexity: 1 })作为兜底为所有没有显式标注复杂度的字段分配默认值 1这与 TypeGraphQL 复杂度为 1 时可省略选项 的约定一致。你也可以继续追加自定义估算器如按类型估算的directiveEstimator等第一个返回数值的估算器生效。方案二graphql-server 的 validationRules旧版示例版本化文档中保留了另一种接入形态通过graphql-server的validationRules配置在每次请求时运行queryComplexity()校验规则const server new GraphQLServer({ schema }); const serverOptions: Options { port: 4000, endpoint: /graphql, playground: /playground, validationRules: req [ queryComplexity({ // The maximum allowed query complexity, queries above this threshold will be rejected maximumComplexity: 8, // The query variables. This is needed because the variables are not available // in the visitor of the graphql-js library variables: req.query.variables, // Optional callback function to retrieve the determined query complexity. // Will be invoked whether the query is rejected or not. // This can be used for logging or to implement rate limiting onComplete: (complexity: number) { console.log(Query Complexity:, complexity); }, estimators: [ // Using fieldConfigEstimator is mandatory to make it work with type-graphql fieldConfigEstimator(), // This will assign each field a complexity of 1 if no other estimator // returned a value. We can define the default value for field not explicitly annotated simpleEstimator({ defaultComplexity: 1, }), ], }), ], }; server.start(serverOptions, ({ port, playground }) { console.log( Server is running, GraphQL Playground available at http://localhost:${port}${playground}, ); });注意此方案中估算器名称为fieldConfigEstimator()旧版 API 命名而当前graphql-query-complexity推荐使用fieldExtensionsEstimator()且默认复杂度阈值maximumComplexity: 8仅为示例值请按业务实际调整。完整实战示例为 Recipe 查询设置复杂度仓库的 examples/query-complexity 目录提供了一个可直接运行的完整示例recipe.type.ts、recipe.resolver.ts、index.ts。对象类型数字与函数两种复杂度用法examples/query-complexity/recipe.type.ts 演示了三种情形ObjectType() export class Recipe { /* By default, every field gets a complexity of 1 */ Field() title!: string; /* Which can be customized by passing the complexity parameter */ Field(_type Int, { complexity: 2 }) ratingsCount!: number; Field(_type Float, { nullable: true, complexity: 10, }) get averageRating(): number | null { const ratingsCount this.ratings.length; if (ratingsCount 0) { return null; } const ratingsSum this.ratings.reduce((a, b) a b, 0); return ratingsSum / ratingsCount; } // Internal property, not exposed in schema ratings!: number[]; }title未标注复杂度按约定取默认值 1ratingsCount复杂度为 2averageRating是一个计算型 getter 字段涉及数组求和与除法成本较高复杂度设为 10。Resolver按参数与子节点复杂度动态计算examples/query-complexity/recipe.resolver.ts 展示了最实用的动态复杂度计算——列表查询的复杂度应随参数count与每个子项的成本相乘Resolver(_of Recipe) export class RecipeResolver implements ResolverInterfaceRecipe { private readonly items: Recipe[] createRecipeSamples(); Query(_returns [Recipe], { /* Pass also a calculation function in the complexity option to determine a custom complexity. This function provides the complexity of the child nodes as well as the field input arguments. That way a more realistic estimation of individual field complexity values is made, e.g. by multiplying childComplexity by the number of items in array */ complexity: ({ childComplexity, args }) args.count * childComplexity, }) async recipes(Arg(count) count: number): PromiseRecipe[] { return this.items.slice(0, count); } /* Complexity in field resolver overrides complexity of equivalent field type */ FieldResolver({ complexity: 5 }) ratingsCount(Root() recipe: Recipe): number { return recipe.ratings.length; } }关键点recipes查询的复杂度 args.count × childComplexity。请求recipes(count: 3)且每个子项包含averageRating成本 10时该查询的复杂度即3 × 10 30超过MAX_COMPLEXITY 20会被拒绝。ratingsCount通过FieldResolver({ complexity: 5 })覆盖了对象类型中Field定义的 2体现了FieldResolver 优先规则。用示例查询验证阈值行为examples/query-complexity/examples.graphql 给出了两条对比查询query GetRecipesWithComplexityError { recipes(count: 3) { title averageRating } } query GetRecipesWithoutComplexityError { recipes(count: 2) { title ratingsCount } }GetRecipesWithComplexityErrorrecipes(count: 3)的averageRating成本 10子项复杂度约 10总复杂度3 × 10 30 20请求被拒绝并抛出Sorry, too complicated query!错误。GetRecipesWithoutComplexityErrorrecipes(count: 2)子项包含title1与ratingsCountFieldResolver 覆盖为 5总复杂度2 × (1 5) 12 ≤ 20请求正常通过。测试用例佐证复杂度的计算是可验证的仓库的 tests/functional/query-complexity.ts 对复杂度计算做了单元级验证可直接证明字段复杂度 默认值 查询总复杂度的算法测试定义了一个complexity: 10的字段complexResolverMethod查询sampleQuery { complexResolverMethod }经fieldExtensionsEstimator()与simpleEstimator({ defaultComplexity: 1 })计算总复杂度期望值等于 11即字段成本 10 根查询的默认成本 1。该测试同样覆盖了 Subscriptions订阅操作场景说明Subscription上的复杂度选项同样会被估算器正确计入。注意事项与最佳实践复杂度只是估算不是真实耗时它衡量的是查询代价上限的近似值适合作为防滥用阈值不应替代真实的性能分析与监控。operationName务必传入避免多操作文档中无关操作被一并计入。阈值要结合业务数据规模设定例如列表查询中count的合理上限、嵌套深度等示例中的 8/20 均为演示值。日志与限流可使用onComplete回调validationRules 方案或在didResolveOperation中记录每次查询的复杂度点数用于监控、告警或实现基于复杂度的限流。不同服务器接入方式可互相替换无论 Apollo Server、express-graphql 还是其他兼容graphql-js的服务器核心都是schema estimators 复杂度阈值这一套组合TypeGraphQL 侧的定义方式完全不变。小结通过 TypeGraphQL 的complexity装饰器选项配合graphql-query-complexity你可以用极小的成本为 GraphQL API 建立一套查询成本预算机制数字定义简单字段成本、函数按参数与子节点动态计算列表类成本FieldResolver优先级覆盖保证灵活性fieldExtensionsEstimator负责读取 TypeGraphQL 写入 schema 的复杂度扩展最终在服务器侧统一校验并拒绝超阈值查询。参考仓库中的 examples/query-complexity 完整示例与 tests/functional/query-complexity.ts 测试即可快速在自己项目中落地这一防护方案。赞分享后端GraphQLAPI设计【免费下载链接】type-graphqlCreate GraphQL schema and resolvers with TypeScript, using classes and decorators!项目地址https://gitcode.com/gh_mirrors/ty/type-graphql点击查看免费下载相关推荐gqlgen 查询复杂度限制用成本计算防御 GraphQL 恶意查询gqlgen 查询复杂度限制用成本计算防御 GraphQL 恶意查询 导读 GraphQL 允许客户端一次请求嵌套极深、规模巨大的数据这种强大能力同样意味着后端GraphQL代码生成深度评测Ascend AI处理器Dhrystone整数运算性能全面分析深度评测Ascend AI处理器Dhrystone整数运算性能全面分析 在AI计算和深度学习应用场景中整数运算性能是衡量处理器核心能力的关键指标。本文基于P算子库人工智能CANNTypeGraphQL查询复杂度估算自定义成本函数TypeGraphQL查询复杂度估算自定义成本函数 GraphQL查询可能引发服务器资源耗尽风险尤其当面对嵌套层级深或数据量大的请求时。TypeGraphQ后端GraphQLAPI设计上一篇如何快速集成Flagsmith移动端SDKiOS和Android完整指南下一篇iOS动画终极指南如何创建惊艳的按钮动画与过渡效果创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

使用 AWS SDK for Kotlin 调用 Amazon Translate:实时翻译与批量翻译任务实战
使用 AWS SDK for Kotlin 调用 Amazon Translate:实时翻译与批量翻译任务实战

示例工程教程后端 【免费下载链接】aws-doc-sdk-examples Welcome to the AWS Code Examples Repository. This repo contains code examples used in the AWS documentation, AWS SDK Developer Guides, and more. For more information, see the Readme.md file below. 项目地… · 2026/9/26 2:14:32

Databasus 复制凭据规格:PostgreSQL 物理备份的 WAL 轮转权限与 PITR 前条件解析
Databasus 复制凭据规格:PostgreSQL 物理备份的 WAL 轮转权限与 PITR 前条件解析

数据库灾备 【免费下载链接】databasus PostgreSQL backup tool with Point-In-Time-Recovery and restore verification 项目地址: https://gitcode.com/gh_mirrors/po/databasus 点击查看 免费下载 导读 本文围绕 Databasus 开源仓库中的复制凭据规格文档展开&a… · 2026/9/26 2:14:32

Blockbench 免费低多边形3D建模与动画完整教程
Blockbench 免费低多边形3D建模与动画完整教程

Blockbench 免费低多边形3D建模与动画完整教程 【免费下载链接】blockbench Blockbench - A low poly 3D model editor 项目地址: https://gitcode.com/GitHub_Trending/bl/blockbench 想给游戏或 Minecraft 做低多边形模型,却被商业软件的价格和陡峭学习曲线劝退?Bloc… · 2026/9/26 2:14:32

故事化内容创作实战:构建叙事钩子与三幕式流程
故事化内容创作实战:构建叙事钩子与三幕式流程

最近埋头做的一个项目,标题就叫“story”。很多人看到这个词觉得太虚、太抽象,但真正把它当成一个完整产品去打磨以后,我发现它其实是几乎所有内容形式的底层骨架——短视频脚本、公众号长文、播客单期,甚至产品里的新手引导流程&… · 2026/9/26 2:53:44

SQL Server数据库实验实战:约束、触发器与游标避坑指南
SQL Server数据库实验实战:约束、触发器与游标避坑指南

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

从脉脉看职场社交生态重构:身份可信度、内容生态与商业化路径
从脉脉看职场社交生态重构:身份可信度、内容生态与商业化路径

职场社交这个赛道,失败案例远比成功案例多。LinkedIn入华多年始终不温不火,腾讯朋友、人人网相继转型,飞书、钉钉内部的社区尝试也始终没有真正长成生态。脉脉算是国内坚持最久、也是唯一把“职场社交”这个命题撑到亿级用户规模的样本。标题… · 2026/9/26 2:53:44

SQL注入原理与实战绕过:从手工注入到参数化防护全解析
SQL注入原理与实战绕过:从手工注入到参数化防护全解析

直接说结论:SQL注入到现在二十多年了,从1998年第一次被公开提出到现在,它依然坚挺地排在OWASP Top 10榜单里,每年因为SQL注入被拖库、被删库、被勒索的事件从来没断过。很多刚入门的安全爱好者总觉得这玩意太老、太基础&#xff0… · 2026/9/26 2:53:44

Cinema 4D 2024 安装教程:环境配置、常见报错与优化指南
Cinema 4D 2024 安装教程:环境配置、常见报错与优化指南

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

品牌设计不是买张图,选对伙伴才能成就好品牌
品牌设计不是买张图,选对伙伴才能成就好品牌

前几天一位做食品加工的老板找我,开口第一句就是:"我想换个包装设计,请问多少钱一个?"我说你别急着问价,我先问你:你现在找的设计师,是坐下来聊过你生意的,还是上来就发过… · 2026/9/26 2:53:38

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

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

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码