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)

工作原理

  1. 每个唯一Key对应一个独立的令牌桶
  2. 令牌以固定速率(1个/秒)补充
  3. 每次请求消耗1个令牌
  4. 令牌不足时触发限流

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限流场景

适用场景

  1. 快速IP封禁:对恶意IP进行快速封禁
  2. 突发流量控制:限制单IP的瞬时请求量
  3. 轻量级限流:无需Redis的简单限流场景
  4. 边缘防护:作为第一道防线阻挡恶意请求

不适用场景

  1. 需要精确全局计数的场景
  2. 对限流精度要求极高的场景
  3. 需要跨服务共享限流状态的场景

核心代码

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

技术参考

posted @ 2025-12-24 14:05  张占岭  阅读(3)  评论(0)    收藏  举报