如果你在项目的后端有一个Node.js GraphQL端点,并有各种解析器,如果你在生产中部署了它,你需要用速率和深度限制来保护你的GraphQL API端点。
速率限制可以帮助你在每次超过设定的请求限制时扼杀用户,而深度限制可以帮助你根据GraphQL查询的深度限制其复杂性。这些措施有助于你的应用程序防止API垃圾和查询攻击。在这篇文章中,我们将介绍为什么以及如何对你的API进行速率限制和深度限制。
什么是速率限制?
速率限制是指限制应用程序或用户在一定时间内可以调用的API数量。如果超过了这个限制,用户或客户端可能会被节流,也就是说,客户端可能被禁止在同一时间段内进行更多类似的API调用。
为什么要对API进行速率限制?
你的后端服务器通常会对它在一个时间段内能处理多少个请求有一些限制。很多时候,有恶意的用户会用垃圾邮件来轰炸你的API端点,这将降低你的服务器速度,甚至可能使它崩溃。
为了保护你的API端点和服务器不被淹没,你必须限制你的API端点,无论是REST API还是GraphQL端点。
速率限制的方法
有多种方法可以限制API,比如说以下几种。
按每一时间段的IP地址
你可以节制某些IP地址,如果他们在某个时间段内超过了API请求的数量,可以限制他们对你的服务的访问。
按用户的时间范围
你可以在你的应用程序上节制某些用户(通过他们在你的数据库中的唯一标识符),如果他们在一个时间范围内超过了API请求的数量,就限制他们对你的服务的访问。
按IP地址和用户的时间范围
在这种方法中,如果用户超过你设置的速率限制,你就根据他们是否使用同一个IP地址来进行节流。
对所有 GraphQL 解析器进行统一的速率限制
当服务器上的 GraphQL 解析器(突变、查询和订阅)具有相同的速率限制时,请这样做,例如每个用户每分钟 10 个请求。
例如,一个GraphQL服务器可能在相同的速率限制规则内有signInMutation ,getUserQuery ,和其他这样的解析器,这意味着在GraphQL解析器之间有一个统一的速率限制)。
每个GraphQL解析器的不同速率限制规则
有时,每个GraphQL解析器都会有不同的速率限制规则。例如,需要繁琐的内存和处理能力的解析器将有更严格的速率限制,与易于处理和不太费时的解析器相比。
当每个时间段允许较少的API请求时,速率限制会更严格。
存储速率限制的数据
为了限制你的API或GraphQL端点的速率,你需要跟踪时间、用户ID、IP地址和/或其他独特的标识符,你还需要存储标识符最后一次请求端点时的数据,以计算标识符是否超过了速率限制。
一个 "标识符 "可以是任何有助于识别客户的唯一字符串,如你数据库中的用户ID、IP地址、两者的字符串组合,甚至是设备信息。
那么,你在哪里存储所有这些数据呢?
Redis是最适合这些用例的数据库。它是一个缓存数据库,你可以在密钥对中保存小段的信息,而且速度极快。
现在我们来安装Redis。稍后,你将能够把它插入你的Node.js GraphQL服务器设置中,以存储速率限制的相关信息。
如果你使用官方文档来安装Redis,在命令行中使用这些命令
wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make
sudo cp src/redis-server /usr/local/bin/ # copying build to proper places
sudo cp src/redis-cli /usr/local/bin/ # copying build to proper places
我个人认为,还有更简单的安装方法。
在Mac上。
brew install redis # to install redis
redis-server /usr/local/etc/redis.conf # to run redis
在Linux上。
sudo apt-get install redis-server # to install redis
redis-server /usr/local/etc/redis.conf # to run redis
Redis服务器启动后,创建一个新用户。在localhost ,没有任何密码也可以运行Redis,但对于生产来说,你不能这样做,因为你不希望你的Redis服务器对互联网开放。
现在让我们设置一个Redis密码。运行redis-cli ,启动Redis命令行。这只有在Redis已经安装并运行的情况下才有效。
然后,在CLI中输入这个命令。
config set requirepass somerandompassword
现在exit 命令行。你的 Redis 服务器现在有了密码保护。
在GraphQL中使用速率限制
在这里,我们将利用graphql-rate-limit npm模块。你还需要[ioredis](https://www.npmjs.com/package/ioredis).
npm i graphql-rate-limit ioredis -s
在本教程中,我使用graphql-yoga服务器作为后端。你也可以使用Apollo GraphQL。
[graphql-rate-limit](https://www.npmjs.com/package/graphql-rate-limit)它可与任何Node.js GraphQL设置一起使用。它所做的一切是创建GraphQL指令,以便在你的GraphQL模式中使用。
这就是一个正常的GraphQL服务器的样子。
import { GraphQLServer } from 'graphql-yoga'
const typeDefs = `
type Query {
hello(name: String!): String!
}
`
const resolvers = {
Query: {
hello: (_, { name }) => `Hello ${name}`
}
}
const server = new GraphQLServer({ typeDefs, resolvers })
server.start(() => console.log('Server is running on localhost:4000'))
现在,让我们用这个代码来限制welcome 查询(解析器)。
import { GraphQLServer } from 'graphql-yoga'
import * as Redis from "ioredis"
import { createRateLimitDirective, RedisStore } from "graphql-rate-limit"
export const redisOptions = {
host: process.env.REDIS_HOST || "127.0.0.1",
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD || "somerandompassword",
retryStrategy: times => {
// reconnect after
return Math.min(times * 50, 2000)
}
}
const redisClient = new Redis(redisOptions)
const rateLimitOptions = {
identifyContext: (ctx) => ctx?.request?.ipAddress || ctx?.id,
formatError: ({ fieldName }) =>
`Woah there, you are doing way too much ${fieldName}`,
store: new RedisStore(redisClient)
}
const rateLimitDirective = createRateLimitDirective(rateLimitOptions)
const resolvers = {
Query: {
hello: (_, { name }) => `Hello ${name}`
}
}
// Schema
const typeDefs = `
directive @rateLimit(
max: Int
window: String
message: String
identityArgs: [String]
arrayLengthField: String
) on FIELD_DEFINITION
type Query {
hello(name: String!): String! @rateLimit(window: "1s", max: 2)
}
`
const server = new GraphQLServer({
typeDefs,
resolvers,
// this enables you to use @rateLimit directive in GraphQL schema.
schemaDirectives: {
rateLimit: rateLimitDirective
}
})
server.start(() => console.log('Server is running on localhost:4000'))
就这样,welcome 的解析器现在被限制了速率。
如果你访问GraphQL Playgroundon [https://localhost:4000](https://localhost:4000)的GraphQL Playground,尝试运行下面的查询。
# Try to spam this query by clicking fast,
# you should see an error message after you hit the rate limit.
query {
hello(name: "Kumar Abhirup")
}
试着快速点击白色的播放按钮,让它变成垃圾邮件,你会碰到速率限制。
在你达到速率限制后,你应该看到设置的错误信息:Woah there, you are doing way too much hello 。
现在让我们深入分解一下代码。
GraphQL中的速率限制选项
const rateLimitOptions = {
identifyContext: (ctx) => ctx?.request?.ipAddress || ctx?.id,
formatError: ({ fieldName }) =>
`Woah there, you are doing way too much ${fieldName}`,
store: new RedisStore(redisClient)
}
const rateLimitDirective = createRateLimitDirective(rateLimitOptions)
identifyContext 是一个函数,它将为所有设备或数据库中的用户返回一个唯一的字符串,或两者的组合。这是你决定是否要按IP地址、按用户ID或按其他方法进行速率限制的地方。
在上面的片段中,我们尝试将用户的IP地址设置为唯一的标识符,如果没有检索到IP地址,它使用默认的GraphQL服务器提供的contextID 作为回退值。
formatError 是一种方法,允许你将用户在遇到速率限制后看到的错误信息格式化。
store 连接到Redis实例,在你的Redis服务器中保存必要的速率限制数据。没有Redis存储,这个速率限制设置就无法运行。请注意,你并不总是要使用Redis作为存储空间--你也可以使用MongoDB或PostgreSQL,但这些数据库对于一个简单的速率限制解决方案来说是多余的。
createRateLimitDirective 是一个函数,如果与rateLimitOptions 一起提供,可以使你创建动态的限速指令,以后可以连接到你的 GraphQL 服务器,在你的模式中使用。
这里是模式。
const typeDefs = `
directive @rateLimit(
max: Int
window: String
message: String
identityArgs: [String]
arrayLengthField: String
) on FIELD_DEFINITION
type Query {
hello(name: String!): String! @rateLimit(window: "1s", max: 2)
}
`
directive @rateLimit 在模式中创建了@rateLimit 指令,该指令接受一些参数,有助于为每个解析器配置速率限制规则。
你可以在你的GraphQL设置完成后,预先将 @rateLimit(window: *"1s"*, max: 2)到任何解析器上,一旦你的GraphQL指令被设置好并准备使用。 window: *"1s"*, max: 2意味着该解析器可以由一个用户、一个IP地址或指定的identifyContext ,每秒只能运行两次。如果在该时间段内第三次运行相同的查询,将出现速率限制错误。
希望你现在知道GraphQL解析器如何进行速率限制,这有助于防止查询垃圾邮件淹没你的服务器。
现在我们已经涵盖了GraphQL速率限制,让我们看看如何用深度限制使你的GraphQL端点更安全。
什么是GraphQL深度限制?
深度限制是指通过深度来限制GraphQL查询的复杂性。GraphQL服务器通常有数据加载器,使用关系型数据库查询加载和填充数据。
看看下面的查询。
query book {
getBook(id: 1) {
title
author
publisher
reviews {
title
body
book {
title
author
publisher
reviews {
title
}
}
}
}
}
它为被查询的book (s)获取了review (s)。每个book 都有rviews ,每个review 都与一个book 相连,这使得数据加载器所查询的是一对多的关系。
现在,看看这个GraphQL查询。
query badMaliciousQuery {
getBook(id: 1) {
reviews {
book {
reviews {
book {
reviews {
book {
reviews {
book {
reviews {
book {
reviews {
book {
# and so on...
}
}
}
}
}
}
}
}
}
}
}
}
}
}
这个查询有几个层次的深度。它创建了一个巨大的循环,可以持续很长时间,这取决于查询的深度,其中book 获取reviews ,reviews 获取books ,以此类推。
这样的查询字符串会使你的GraphQL服务器不堪重负,并可能使其崩溃。想象一下,发送一个10,000级深度的查询--这将是灾难性的!这就是深度限制的作用。
这就是深度限制的作用。它使GraphQL服务器能够检测到这样的查询,并防止它们被处理为警告。
还有另一种方法用来解决这个问题,叫做 "请求超时 "错误,如果解决时间过长,就会阻止解析器执行一个查询。
深度限制GraphQL API
这是一个相当简单的过程。我们可以用 [graphql-depth-limit](https://www.npmjs.com/package/graphql-depth-limit)来深度限制GraphQL查询。
import { GraphQLServer } from 'graphql-yoga'
import * as depthLimit from 'graphql-depth-limit'
const typeDefs = `
type Query {
hello(name: String!): String!
}
`
const resolvers = {
Query: {
hello: (_, { name }) => `Hello ${name}`
}
}
const server = new GraphQLServer({
typeDefs,
resolvers,
// easily set a depth limit on all the incoming graphql queries
// here, we set a depth limit of 7
validationRules: [depthLimit(7)]
})
server.start(() => console.log('Server is running on localhost:4000'))
我们都准备好了!还有很多其他的方法,你可以使用深度限制来限制查询的复杂性。
结论
为了防止你的GraphQL服务器被API请求淹没,速率限制和深度限制你的GraphQL端点是必须的,它还可以保护你的服务器免受恶意的查询攻击,这些攻击可以使你的解析器处于无休止的请求循环中,特别是当你为一个实时应用程序部署服务器时。
The post Securing GraphQL API using rate limits and depth limitsappeared first onLogRocket Blog.