速率限制HTTP请求(通过http.HandlerFunc中间件)

时间:2013-11-30 08:34:09

标签: http go

我正在寻找一小部分限速中间件:

  1. 允许我为每个远程IP设置合理的费率(例如,10 req / s)
  2. 可能(但并非必须)允许连发
  3. 丢弃(关闭?)超过速率的连接并返回HTTP 429
  4. 然后我可以围绕身份验证路由或其他可能容易遭受暴力攻击的路由(即使用到期的令牌密码重置URL等)。有人粗暴地强制使用16或24字节令牌的可能性非常低,但进行额外步骤并不会有什么坏处。

    我已经查看了https://code.google.com/p/go-wiki/wiki/RateLimiting,但我不确定如何将其与http.Request(s)进行协调。此外,我不确定我们如何跟踪"在任何时间段内来自特定IP的请求。

    理想情况下,我最终会得到类似的结果,并指出我在反向代理(nginx)后面,所以我们要检查REMOTE_ADDR HTTP标头,而不是{ {1}}:

    r.RemoteAddr

    我很欣赏这里的一些指导。

4 个答案:

答案 0 :(得分:11)

您链接的限速示例是一般的。它使用范围,因为它通过通道获取请求。

这是一个与HTTP请求不同的故事,但这里没有什么真正复杂的。请注意,您不会迭代请求通道或任何内容 - 为每个传入请求分别调用HandlerFunc

func rateLimit(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        remoteIP := r.Header.Get("REMOTE_ADDR")
        if exceededTheLimit(remoteIP) {
            w.WriteHeader(429)
            // it then returns, not passing the request down the chain
        } else {
            h.ServeHTTP(w, r);
        }
    }       
}

现在,选择存储速率限制计数器的位置取决于您。一种解决方案是简单地使用将IP映射到其请求计数器的全局映射(不要忘记安全的并发访问)。但是,您必须知道请求的持续时间。

Sergio建议使用Redis。它的键值特性非常适合这种简单的结构,你可以免费使用。

答案 1 :(得分:4)

您可以将数据存储在redis中。这是一个非常有用的命令,甚至在其文档中提到了速率限制应用程序:INCR。 Redis还将处理旧数据的清理(通过旧密钥到期)。

此外,由于redis是速率限制器存储,您可以使用共享此中央存储的多个前端进程。

有人会争辩说,每次进入外部流程都很昂贵。但密码重置页面不是一种绝对要求最佳性能的页面。此外,如果将redis放在同一台机器上,延迟应该非常低。

答案 2 :(得分:4)

今天早上我做了一些简单而类似的事情,我认为这可以帮助你解决问题。

package main

import (
    "log"
    "net/http"
    "strings"
    "time"
)

func main() {
    fs := http.FileServer(http.Dir("./html/"))
    http.Handle("/", fs)
    log.Println("Listening..")
    go clearLastRequestsIPs()
    go clearBlockedIPs()
    err := http.ListenAndServe(":8080", middleware(nil))
    if err != nil {
        log.Fatalln(err)
    }
}

// Stores last requests IPs
var lastRequestsIPs []string

// Block IP for 6 hours
var blockedIPs []string

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ipAddr := strings.Split(r.RemoteAddr, ":")[0]
        if existsBlockedIP(ipAddr) {
            http.Error(w, "", http.StatusTooManyRequests)
            return
        }
        // how many requests the current IP made in last 5 mins
        requestCounter := 0
        for _, ip := range lastRequestsIPs {
            if ip == ipAddr {
                requestCounter++
            }
        }
        if requestCounter >= 1000 {
            blockedIPs = append(blockedIPs, ipAddr)
            http.Error(w, "", http.StatusTooManyRequests)
            return
        }
        lastRequestsIPs = append(lastRequestsIPs, ipAddr)

        // Don't cut the chain of middlewares
        if next == nil {
            http.DefaultServeMux.ServeHTTP(w, r)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func existsBlockedIP(ipAddr string) bool {
    for _, ip := range blockedIPs {
        if ip == ipAddr {
            return true
        }
    }
    return false
}

func existsLastRequest(ipAddr string) bool {
    for _, ip := range lastRequestsIPs {
        if ip == ipAddr {
            return true
        }
    }
    return false
}

// Clears lastRequestsIPs array every 5 mins
func clearLastRequestsIPs() {
    for {
        lastRequestsIPs = []string{}
        time.Sleep(time.Minute * 5)
    }
}

// Clears blockedIPs array every 6 hours
func clearBlockedIPs() {
    for {
        blockedIPs = []string{}
        time.Sleep(time.Hour * 6)
    }
}

它仍然不精确,但是,作为速率限制器的简单示例,它会有所帮助。您可以通过添加请求的路径,http方法甚至身份验证作为因素来改进它,以确定流是否是攻击。

答案 3 :(得分:2)

这是我的速率限制中间件实现。它作为全局速率限制器或单个请求的速率限制器非常好用。我在我的应用程序中广泛使用它。

这是你得到的:

  • 没有外部依赖
  • 可测试
  • 配置
  • 添加标题,以便客户可以了解在限制之前剩余的请求数等等。
  • 自动删除过期数据。

首先,实施:

r := router.New()
stats := stats.New()
r.With(middleware.RateLimit(1, time.Minute * 1, stats)).Post("/contact", c.Contact)

在向POST提出/contact请求时,中间件将允许一个请求宠物分钟。

这是中间件:

package middleware

import (
    "net/http"
    "strconv"
    "time"
)

// Stats is an interface to an underlying hash table/map data
// structure. Implement it however you'd like.
type Stats interface {
    // Reset will reset the map.
    Reset()

    // Add would add "count" to the map at the key of "identifier",
    // and returns an int which is the total count of the value 
    // at that key.
    Add(identifier string, count int) int
}

// RateLimit middleware is a generic rate limiter that can be used in any scenario
// because it allows granular rate limiting for each specific request. Or you can
// set the rate limiter on the entire router group. It's just a HandlerFunc.
func RateLimit(limit int, window time.Duration, stats Stats) func(next http.Handler) http.Handler {
    var windowStart time.Time

    // Clear the rate limit stats after each window.
    ticker := time.NewTicker(window)
    go func() {
        windowStart = time.Now()

        for range ticker.C {
            windowStart = time.Now()
            stats.Reset()
        }
    }()

    return func(next http.Handler) http.Handler {
        h := func(w http.ResponseWriter, r *http.Request) {
            value := int(stats.Add(identifyRequest(r), 1))

            XRateLimitRemaining := limit - value
            if XRateLimitRemaining < 0 {
                XRateLimitRemaining = 0
            }

            w.Header().Add("X-Rate-Limit-Limit", strconv.Itoa(limit))
            w.Header().Add("X-Rate-Limit-Remaining", strconv.Itoa(XRateLimitRemaining))
            w.Header().Add("X-Rate-Limit-Reset", strconv.Itoa(int(window.Seconds()-time.Since(windowStart).Seconds())+1))

            if value >= limit {
                w.WriteHeader(429)
                // Do something else...
            } else {
                next.ServeHTTP(w, r)
            }
        }

        return http.HandlerFunc(h)
    }
}

// identifyRequest gets an identifier from the request context.
func identifyRequest(r *http.Request) string {
    // Identify your request here (get IP address, etc.)
}