wasm~tinygo写一个按着来源IP进行限流的插件
IP Limit 插件
插件功能
基于IP地址的限流插件,使用令牌桶算法对客户端IP进行访问频率限制,超过限制后会锁定一段时间。
核心特性
- 基于客户端IP地址进行限流
- 使用
golang.org/x/time/rate实现令牌桶算法 - 超限后自动锁定,锁定期间拒绝所有请求
- 无需依赖外部存储(Redis)
- 每个 WASM VM 线程独立计数
实现逻辑
1. 请求处理流程
请求到达 → 生成限流Key → 检查是否被锁定 → 令牌桶判断 → 放行/拒绝+锁定
2. 核心组件
配置解析 (parseConfig)
| 参数 | 默认值 | 说明 |
|---|---|---|
| ttlSecond | 60 | 锁定时间(秒) |
| burst | 3 | 令牌桶容量(并发数) |
限流Key生成
keyStr := clientIP + "_" + host + "_" + path
key := md5(keyStr)
Key格式:{客户端IP}_{域名}_{请求路径} 的 MD5 值
3. 令牌桶算法
使用 rate.NewLimiter(1, burst) 创建限流器:
- 速率(Rate): 1 token/秒
- 容量(Burst): 可配置,默认3个令牌
type visitor struct {
limiter *rate.Limiter // 令牌桶限流器
lastSeen time.Time // 最后访问时间
}
var visitors = make(map[string]*visitor)
工作原理
- 每个唯一Key对应一个独立的令牌桶
- 令牌以固定速率(1个/秒)补充
- 每次请求消耗1个令牌
- 令牌不足时触发限流
4. 锁定机制
当令牌桶判断超限时,使用 SharedData 机制进行全局锁定:
// 锁定逻辑
if !limiter.Allow() {
ttl := time.Duration(config.ttlSecond) * time.Second
expireTime := time.Now().Add(ttl)
// 存储到 SharedData(跨线程共享)
proxywasm.SetSharedData(key, timeBytes, cas)
// 返回 429
proxywasm.SendHttpResponse(429, nil, errorMessage, -1)
}
锁定检查
// 请求到达时首先检查锁定状态
timeBytes, _, err := proxywasm.GetSharedData(key)
if err == nil && time.Now().Before(decodedTime) {
// 仍在锁定期,拒绝请求
return 429
}
5. 数据隔离说明
线程隔离
- 每个 WASM VM 运行在隔离的线程中
visitors变量在各线程中独立- 不同线程的令牌桶计数不共享
跨线程共享
- 锁定状态使用
proxywasm.SharedData机制 - 锁定信息可以跨线程共享
- 一旦某个线程锁定了IP,其他线程也能感知
6. 限流响应
超过限制时返回:
HTTP 429 Too Many Requests
Body:
this key address ({key}) too many requests, locked ({ttlSecond}) seconds.
配置参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| ttlSecond | int | 否 | 60 | 超限后锁定时间(秒) |
| burst | int | 否 | 3 | 令牌桶容量/并发数 |
配置示例
ttlSecond: 120
burst: 5
与其他限流插件的区别
| 特性 | IP Limit | Global Limit | Route Limit | Session Limit |
|---|---|---|---|---|
| 限流维度 | IP+Host+Path | 全局 | URL | 会话 |
| 算法 | 令牌桶 | 滑动窗口 | 滑动窗口 | 固定窗口 |
| 依赖Redis | 否 | 是 | 是 | 是 |
| 锁定机制 | 有 | 无 | 无 | 无 |
| 跨线程计数 | 部分共享 | 完全共享 | 完全共享 | 完全共享 |
注意事项
线程独立性
由于每个 WASM VM 运行在独立线程中:
- 令牌桶计数在各线程独立
- 实际限流效果 = 配置值 × 线程数
- 适合粗粒度的IP限流场景
适用场景
- 快速IP封禁:对恶意IP进行快速封禁
- 突发流量控制:限制单IP的瞬时请求量
- 轻量级限流:无需Redis的简单限流场景
- 边缘防护:作为第一道防线阻挡恶意请求
不适用场景
- 需要精确全局计数的场景
- 对限流精度要求极高的场景
- 需要跨服务共享限流状态的场景
核心代码
keyStr := util.GetClientIP() + "_" + ctx.Host() + "_" + ctx.Path()
key := util.Md5String(keyStr)
err := fmt.Sprintf("this key address (%s) too many requests,locked (%d) seconds.", key, config.ttlSecond)
// 判断是否在全局缓存中,并且是否过期
timeBytes, _, err0 := proxywasm.GetSharedData(key)
if err0 == nil {
var decodedTime time.Time
if timeBytes != nil {
err1 := json.Unmarshal(timeBytes, &decodedTime)
if err1 == nil {
if time.Now().Before(decodedTime) {
proxywasm.SendHttpResponse(ERR_CODE, nil, []byte(err), -1) //直接响应结果
return types.ActionContinue
}
}
}
}
limiter := getVisitor(key, config.burst)
if false == limiter.Allow() {
ttl := time.Duration(config.ttlSecond) * time.Second
// 添加全局缓存,过期时间为ttl
t := time.Now().Add(ttl)
timeBytes, err1 := json.Marshal(t)
if err1 == nil {
proxywasm.SetSharedData(key, timeBytes, 1)
}
proxywasm.SendHttpResponse(ERR_CODE, nil, []byte(err), -1) //直接响应结果
return types.ActionContinue
}
return types.ActionContinue
浙公网安备 33010602011771号