golang接口IP限流,IP黑名单,IP白名单的实例

时间:2022-05-22 03:36:11

增加中间件

可以选择普通模式和LUA脚本模式,建议选择普通模式,实际上不需要控制的那么精确。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package Middlewares
import (
    "github.com/gin-gonic/gin"
    "strconv"
    "time"
    "voteapi/pkg/app/response"
    "voteapi/pkg/gredis"
    "voteapi/pkg/util"
)
const IP_LIMIT_NUM_KEY = "ipLimit:ipLimitNum"
const IP_BLACK_LIST_KEY = "ipLimit:ipBlackList"
var prefix = "{gateway}"
var delaySeconds int64 = 60  // 观察时间跨度,秒
var maxAttempts int64 = 10000 // 限制请求数
var blackSeconds int64 = 0  // 封禁时长,秒,0-不封禁
func GateWayPlus() gin.HandlerFunc {
    return func(c *gin.Context) {
        path := c.FullPath()
        clientIp := c.ClientIP()
        // redis配置集群时必须
        param := make(map[string]string)
        param["path"] = path
        param["clientIp"] = clientIp
        if !main(param) {
            c.Abort()
            response.JsonResponseError(c, "当前IP请求过于频繁,暂时被封禁~")
        }
    }
}
func main(param map[string]string) bool {
    // 预知的IP黑名单
    var blackList []string
    if util.InStringArray(param["clientIp"], blackList) {
        return false
    }
    // 预知的IP白名单
    var whiteList []string
    if util.InStringArray(param["clientIp"], whiteList) {
        return false
    }
    blackKey := prefix + ":" + IP_BLACK_LIST_KEY
    limitKey := prefix + ":" + IP_LIMIT_NUM_KEY
    curr := time.Now().Unix()
    item := util.Md5(param["path"] + "|" + param["clientIp"])
    return normal(blackKey, limitKey, item, curr)
}
// 普通模式
func normal(blackKey string, limitKey string, item string, time int64) (res bool) {
    if blackSeconds > 0 {
        timeout, _ := gredis.RawCommand("HGET", blackKey, item)
        if timeout != nil {
            to, _ := strconv.Atoi(string(timeout.([]uint8)))
            if int64(to) > time {
                // 未解封
                return false
            }
            // 已解封,移除黑名单
            gredis.RawCommand("HDEL", blackKey, item)
        }
    }
    l, _ := gredis.RawCommand("HGET", limitKey, item)
    if l != nil {
        last, _ := strconv.Atoi(string(l.([]uint8)))
        if int64(last) >= maxAttempts {
            return false
        }
    }
    num, _ := gredis.RawCommand("HINCRBY", limitKey, item, 1)
    if ttl, _ := gredis.TTLKey(limitKey); ttl == int64(-1) {
        gredis.Expire(limitKey, int64(delaySeconds))
    }
    if num.(int64) >= maxAttempts && blackSeconds > 0 {
        // 加入黑名单
        gredis.RawCommand("HSET", blackKey, item, time+blackSeconds)
        // 删除记录
        gredis.RawCommand("HDEL", limitKey, item)
    }
    return true
}
// LUA脚本模式
// 支持redis集群部署
func luaScript(blackKey string, limitKey string, item string, time int64) (res bool) {
    script := `
local blackSeconds = tonumber(ARGV[5])
if(blackSeconds > 0)
then
  local timeout = redis.call('hget', KEYS[1], ARGV[1])
  if(timeout ~= false)
  then
    if(tonumber(timeout) > tonumber(ARGV[2]))
    then
      return false
    end
    redis.call('hdel', KEYS[1], ARGV[1])
  end
end
local last = redis.call('hget', KEYS[2], ARGV[1])
if(last ~= false and tonumber(last) >= tonumber(ARGV[3]))
then
  return false
end
local num = redis.call('hincrby', KEYS[2], ARGV[1], 1)
local ttl = redis.call('ttl', KEYS[2])
if(ttl == -1)
then
  redis.call('expire', KEYS[2], ARGV[4])
end
if(tonumber(num) >= tonumber(ARGV[3]) and blackSeconds > 0)
then
  redis.call('hset', KEYS[1], ARGV[1], ARGV[2] + ARGV[5])
  redis.call('hdel', KEYS[2], ARGV[1])
end
return true
`
    result, err := gredis.RawCommand("EVAL", script, 2, blackKey, limitKey, item, time, maxAttempts, delaySeconds, blackSeconds)
    if err != nil {
        return false
    }
    if result == int64(1) {
        return true
    } else {
        return false
    }
}

补充:golang实现限制每秒多少次的限频操作

前言

一些函数的执行可能会限制频率,比如某个api接口要求每秒最大请求30次。下面记录了自己写的限频和官方的限频

代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 加锁限频,输出次数大概率小于最大值
func ExecLimit(lastExecTime *time.Time, l *sync.RWMutex ,maxTimes int, perDuration time.Duration, f func()) {
  l.Lock()
  defer l.Unlock()
 // per times cost time(s)
 SecondsPerTimes := float64(perDuration) / float64(time.Second) / float64(maxTimes)
 now := time.Now()
 interval := now.Sub(*lastExecTime).Seconds()
 if interval < SecondsPerTimes {
 time.Sleep(time.Duration(int64((SecondsPerTimes-interval)*1000000000)) * time.Nanosecond)
 }
 f()
 *lastExecTime = time.Now()
}
// 官方的,需要引用 "golang.org/x/time/rate"
// 基本上可以达到满值,比自己写的更优
func ExecLimit2(l *rate.Limiter, f func()) {
 go func() {
 l.Wait(context.Background())
 f()
 }()
}

使用

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func TestExecLimit(t *testing.T) {
 runtime.GOMAXPROCS(runtime.NumCPU())
 go func() {
 var lastExecTime time.Time
 var l sync.RWMutex
 for {
  ExecLimit(&lastExecTime, &l, 10, time.Second, func() {
  fmt.Println("do")
  })
 }
 }()
 select {
 case <-time.After(1 * time.Second):
 fmt.Println("1秒到时")
 }
}
func TestExecLimit2(t *testing.T) {
 runtime.GOMAXPROCS(runtime.NumCPU())
 l := rate.NewLimiter(1, 30)
 go func() {
 for {
      ExecLimit2(l, func() {
  fmt.Println("do")
  })
 }
 }()
 select {
 case <-time.After(1 * time.Second):
 fmt.Println("1秒到时")
 }
}

输出:

一秒内输出了<=10次 "do"

如何在多节点服务中限制频

上述使用,定义在某个服务节点的全局变量lastExecTime仅仅会对该服务的函数f()操作限频,如果在负载均衡后,多个相同服务的节点,对第三方的接口累计限频,比如三个服务共同拉取第三方接口,合计限频为30次/s.

则,必须将lastExecTime的获取,从redis等共享中间件中获取,而不应该从任何一个单点服务获取。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。如有错误或未考虑完全的地方,望不吝赐教。

原文链接:https://blog.csdn.net/raoxiaoya/article/details/108997674