来源:hgdjnet-server 9bfbafe10d417deea959bd53117bc88a10e60a83 项目身份 HKDJ / JHA-216,独立官网仓库,Jenkins 守卫键 hkdjnet 测试目录 /server/hkdj_net,App/Web API 30331/30332 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
85 lines
1.6 KiB
Go
85 lines
1.6 KiB
Go
package apilimitHandler
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type counter struct {
|
|
Count int
|
|
WindowStart time.Time
|
|
}
|
|
|
|
type RouteHourlyLimiter struct {
|
|
mu sync.Mutex
|
|
limit int
|
|
window time.Duration
|
|
counters map[string]*counter
|
|
}
|
|
|
|
func NewRouteHourlyLimiter(limit int) *RouteHourlyLimiter {
|
|
l := &RouteHourlyLimiter{
|
|
limit: limit,
|
|
window: time.Hour,
|
|
counters: make(map[string]*counter),
|
|
}
|
|
go l.cleaner()
|
|
return l
|
|
}
|
|
|
|
func (l *RouteHourlyLimiter) Middleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// 用 FullPath 做“接口维度”,例如 /api/user/:id
|
|
route := c.FullPath()
|
|
if route == "" {
|
|
// 路由未匹配时兜底
|
|
route = c.Request.URL.Path
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
item, ok := l.counters[route]
|
|
if !ok || now.Sub(item.WindowStart) >= l.window {
|
|
l.counters[route] = &counter{
|
|
Count: 1,
|
|
WindowStart: now,
|
|
}
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
if item.Count >= l.limit {
|
|
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
|
"code": http.StatusTooManyRequests,
|
|
"message": "rate limit exceeded: max 100 requests per route per hour",
|
|
})
|
|
return
|
|
}
|
|
|
|
item.Count++
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// 定时清理过期窗口,避免 map 无限增长
|
|
func (l *RouteHourlyLimiter) cleaner() {
|
|
ticker := time.NewTicker(10 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for now := range ticker.C {
|
|
l.mu.Lock()
|
|
for k, v := range l.counters {
|
|
if now.Sub(v.WindowStart) >= l.window {
|
|
delete(l.counters, k)
|
|
}
|
|
}
|
|
l.mu.Unlock()
|
|
}
|
|
}
|