检查点 💧

一个使用 Redis 数据库为 Vapor 服务器实现速率限制的中间件。

...

let tokenBucket = TokenBucket {
	TokenBucketConfiguration(bucketSize: 25,
							 refillRate: 5,
							 refillTimeInterval: .seconds(count: 45),
							 appliedField: .header(key: "X-ApiKey"),
							 scope: .endpoint)
} storage: {
	application.redis("rate")
} logging: {
	application.logger
}


let checkpoint = Checkpoint(using: tokenBucket)

// 🚨 Modify response HTTP header and body response when rate limit exceed
checkpoint.didFailWithTooManyRequest = { (request, response, metadata) in
	metadata.headers = [
		"X-RateLimit" : "Failure for request \(request.id)."
	]
	
	metadata.reason = "Rate limit for your api key exceeded"
}

// 💧 Vapor Middleware
app.middleware.use(checkpoint)

支持的算法

目前 Checkpoint 支持 4 种速率限制算法。

令牌桶

令牌桶速率限制算法是一种被广泛使用的灵活方法,它控制对服务的请求速率,同时允许一些流量突发。 以下是对其工作原理的解释

令牌桶的配置使用 TokenBucketConfiguration 类型设置

let tokenbucketAlgorithm = TokenBucket {
	TokenBucketConfiguration(bucketSize: 10,
							 refillRate: 0,
							 refillTimeInterval: .seconds(count: 20),
							 appliedTo: .header(key: "X-ApiKey"),
							 inside: .endpoint)
} storage: {
	// Rate limit database in Redis
	app.redis("rate").configuration = try? RedisConfiguration(hostname: "localhost",
															 port: 9090,
															 database: 0)
	
	return app.redis("rate")
} logging: {
	app.logger
}

令牌桶算法如何工作

  1. 初始化桶
  1. 处理传入请求
  1. 添加令牌

漏桶

漏桶速率限制算法是速率限制的有效方法,可确保平稳、稳定的请求流。 它的工作原理类似于底部有一个孔的物理桶,水(请求)以恒定速率滴出。 以下是对其工作原理的详细说明

漏桶的配置是 LeakingBucketConfiguration 对象

let leakingBucketAlgorithm = LeakingBucket {
	LeakingBucketConfiguration(bucketSize: 10,
							   removingRate: 5,
							   removingTimeInterval: .minutes(count: 1),
							   appliedTo: .header(key: "X-ApiKey"),
							   inside :.endpoint)
} storage: {
	// Rate limit database in Redis
	app.redis("rate").configuration = try? RedisConfiguration(hostname: "localhost",
															 port: 9090,
															 database: 0)
	
	return app.redis("rate")
} logging: {
	app.logger
}

漏桶算法如何工作

  1. 初始化桶
  1. 处理传入请求
  1. 处理请求

固定窗口计数器

固定窗口计数器速率限制算法是一种简单易于实现的速率限制方法,用于控制客户端在指定时间段内可以向服务发出的请求数量。 以下是对其工作原理的解释

要设置配置,您必须使用 FixedWindowCounterConfiguration 类型

let fixedWindowAlgorithm = FixedWindowCounter {
	FixedWindowCounterConfiguration(requestPerWindow: 10,
									timeWindowDuration: .minutes(count: 2),
									appliedTo: .header(key: "X-ApiKey"),
									inside: .endpoint)
} storage: {
	// Rate limit database in Redis
	app.redis("rate").configuration = try? RedisConfiguration(hostname: "localhost",
															 port: 9090,
															 database: 0)
	
	return app.redis("rate")
} logging: {
	app.logger
}

固定窗口计数器算法如何工作

  1. 定义时间窗口:选择一个固定的持续时间(例如,1 分钟、1 小时),它将作为计算请求的时间窗口。

  2. 初始化计数器:为每个客户端(或每个被访问的资源)维护一个计数器,以跟踪当前时间窗口内发出的请求数。

  3. 跟踪请求时间戳:每次发出请求时,检查当前时间戳并确定它属于哪个时间窗口。 增加计数器

  1. 强制限制

滑动窗口日志

与固定窗口计数器相比,滑动窗口日志速率限制算法是一种更精细的速率限制方法。 它通过维护单个请求时间戳的日志来提供对请求速率的更平滑控制,从而允许更精细和准确的速率限制机制。 以下是对其工作原理的详细说明

要为此速率限制算法设置配置,请使用 `` 类型

let slidingWindowLogAlgorith = SlidingWindowLog {
	SlidingWindowLogConfiguration(requestPerWindow: 10,
								  windowDuration: .minutes(count: 2),
								  appliedTo: .header(key: "X-ApiKey"),
								  inside: .endpoint)
} storage: {
	// Rate limit database in Redis
	app.redis("rate").configuration = try? RedisConfiguration(hostname: "localhost",
															 port: 9090,
															 database: 0)
	
	return app.redis("rate")
} logging: {
	app.logger
}

滑动窗口日志算法如何工作

  1. 定义时间窗口:选择一个时间窗口持续时间(例如,1 分钟),您希望在该时间内限制请求数量。

  2. 记录请求:为每个客户端维护一个日志(通常是列表或队列),用于存储每个请求的时间戳。

  3. 处理传入请求:当新请求到达时,执行以下操作

修改服务器响应

有时我们需要通过添加自定义 HTTP 标头或在 JSON payload 中设置失败原因文本来修改发送给客户端的响应。

在这种情况下,您可以使用 Checkpoint 类中定义的闭包之一,每个速率限制处理阶段一个。

在执行速率限制检查之前

此闭包在 Checkpoint 中间件对给定请求执行检查操作之前被调用,并接收一个 Request 对象作为参数。

public var willCheck: CheckpointAction?

在执行速率限制检查之后

如果速率限制检查进行顺利,则调用此闭包,并且您知道该 Request 将继续由 Vapor 服务器处理。

public var willCheck: CheckpointAction?

达到速率限制

您肯定想知道何时请求达到您在初始化 Checkpoint 时设置的速率限制。

在这种情况下,Checkpoint 将使用 didFailWithTooManyRequest 闭包通知达到速率限制。

public var didFailWithTooManyRequest: CheckpointErrorAction?

此闭包包含 3 个参数

例如,如果您想添加一个自定义 HTTP 标头和一个原因文本来通知用户他达到了限制,您将执行类似以下的操作

// 👮‍♀️ Modify response HTTP header and body response when rate limit exceed
checkpoint.didFailWithTooManyRequest = { (request, response, metadata) in
	metadata.headers = [
		"X-RateLimit" : "Failure for request \(request.id)."
	]
	
	metadata.reason = "Rate limit for your api key exceeded"
}

处理请求时抛出错误

如果来自 Checkpoint 的错误不同于 HTTP 429 代码(速率限制),您将在以下闭包中获得报告

// 🚨 Modify response HTTP header and body response when error occurs
checkpoint.didFail = { (request, response, abort, metadata) in
	metadata.headers = [
		"X-ApiError" : "Error for request \(request.id)."
	]
	
	metadata.reason = "Error code \(abort.status) for your api key exceeded"
}

此闭包中使用的参数与闭包中接收的参数相同,您可以添加自定义 HTTP 标头和/或原因消息。

Redis

要使用 Checkpoint,您必须在系统中安装和配置 Redis 数据库。 借助 Docker,部署 Redis 安装非常容易。

我们建议从 Docker Hub 安装 redis-stack-server 镜像。

历史

0.1.0

Alpha 版本,一个亲友版本😜