用GraphQL限制查询所获取的数据
首先
当从GraphQL客户端调用查询时,如果没有限制要获取的数据。
有可能收到意料之外的大量数据请求或受到恶意攻击的危险。
为了防止这种情况发生,可以在GraphQL服务器端采取措施。
环境
-
- GraphQL Server
Node.js
Express
MongoDB
Apollo Server
GraphQL Client
React.js
Apollo Client
数据限制
假设以下的模式查询。
type Photo {
id: ID!
url: String!
name: String!
postedBy: User!
taggedUsers: [User!]!
created: DateTime!
}
type User {
name: String
avatar: String
postedPhotos: [Photo!]!
inPhotos: [Photo!]!
}
query allPhotos {
allPhotos(first=99999) {
name
url
postedBy {
name
avatar
}
}
}
如果在查询参数中添加first参数,就可以指定要返回的数据记录数量。在上述查询中,由于指定了first参数为99999,所以会发生获取99999条数据的情况。
限制决解者
通过在查询解析器中添加参数检查处理,限制数据大小。
allPhots: (root, data, context) {
if (data.first > 100) {
throw new Erroe('Only 100 photos can be requested at a time')
}
}
查询深度的限制
GraphQL提供了一个优点,即可以一次性查询相关的数据。例如,假设有一个名为Photo的API。
query getPhoto($id: ID!) {
Photo(id: $id) {
name
url
postedBy {
name
avatar
postedPhotos {
name
url
}
}
}
}
由于查询涉及到两个相关字段postedBy和postedPhoto,可以说查询的深度为3。
query getPhoto($id: ID!) {
Photo(id: $id) {
name # 深さ:1
url # 深さ:1
postedBy {
name # 深さ:2
avatar # 深さ:2
postedPhotos {
name # 深さ:3
url # 深さ:3
}
}
}
}
此外,利用上述的查询,您还可以发出深度更深的查询,如下所示。
query getPhoto($id: ID!) {
Photo(id: $id) {
name # 深さ:1
url # 深さ:1
postedBy {
name # 深さ:2
avatar # 深さ:2
postedPhotos {
name # 深さ:3
url # 深さ:3
taggedUsers {
name # 深さ:4
avatar # 深さ:4
postedPhots {
name # 深さ:5
url # 深さ:5
}
}
}
}
}
}
当查询的深度增加时,服务器的负载会呈指数级增长。
为了不允许这样的查询,也可以使用npm包来实现查询的深度限制。
npm install graphql-depth-limit
安装上述软件包后,您可以使用depthLimit函数将验证规则添加到GraphQL服务器的设置中。
const { ApolloServer } = require(`apollo-server-express`);
const depthLimit = require('graphql-depth-limit');
.....
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)],
.....
})
查询复杂性的限制
接下来我们考虑查询复杂度高的情况下的查询。
即使像上面那样查询的深度并不太深,但是有许多被询问的字段会导致查询负载较高。
query everything($id: ID!) {
totalUsers # 深さ:1
Photo(id: $id) {
name # 深さ:1
url # 深さ:1
}
allUsers {
id # 深さ:2
name # 深さ:2
avatar # 深さ:2
postedPhotos {
name # 深さ:3
url # 深さ:3
}
inPhotos {
name # 深さ:3
url # 深さ:3
taggedUsers {
id # 深さ:4
}
}
}
}
所有的查询在查询深度限制为5以内,但查询的字段数量很多,会导致非常高的计算成本。
还有支持实施查询复杂度限制的npm包。
npm install graphql-validation-complexity
这个graphql-validation-complexity规定了确定查询复杂度的规则。
每个标量字段都设定为值1,如果字段是列表,则增加的值是原来值的10倍。
query everything($id: ID!) {
totalUsers # 複雑度:1
Photo(id: $id) {
name # 複雑度:1
url # 複雑度:1
}
allUsers {
id # 複雑度:10
name # 複雑度:10
avatar # 複雑度:10
postedPhotos {
name # 複雑度:100
url # 複雑度:100
}
inPhotos {
name # 複雑度:100
url # 複雑度:100
taggedUsers {
id # 複雑度:1000
}
}
}
} # 総複雑度1433
将查询的复杂度限制设为1000,可以防止执行此类特殊查询。
const { createComplexityLimitRule } = require('graphql-validation-complexity')
.....
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5),
createComplexityLimitRule(1000, {
onCost: cost => console.log('query cost: ', cost)
})
],
.....
}
在这个例子中,我们将最大复杂度限制为1000。
另外,还实现了一个onCost函数,当计算每个查询的总成本时,将该值作为参数进行调用。
由于先前的查询的复杂度超过了1000,根据这个条件下不允许执行。
最后
我发现通过在GraphQL服务器和客户端中分别进行设置,可以防止发出负载较重的查询。
我认为不仅不需要庞大的数据需求,而且还对恶意攻击有一定的效果,所以我希望在实务中积极采用。