用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服务器和客户端中分别进行设置,可以防止发出负载较重的查询。

我认为不仅不需要庞大的数据需求,而且还对恶意攻击有一定的效果,所以我希望在实务中积极采用。

广告
将在 10 秒后关闭
bannerAds