-- /usr/local/lua/access_limit.lua -- ===== 配置区(建议外置到 nginx.conf 用 set_by_lua)===== local pool_max_idle_time = 10000 local pool_size = 100 local redis_connection_timeout = 100 local redis_host = "your redis host ip" local redis_port = 6379 local redis_auth = "your redis password" local ip_block_time = 120-- 封禁时长(秒) local ip_time_out = 1-- 限频窗口(秒) local ip_max_count = 3-- 窗口内最大访问次数 -- 受信任的代理层数(CDN + LB + Nginx 自己 = 视部署而定,0 = 不信 X-Forwarded-For) local trusted_proxy_hops = 0 -- ===== 工具函数 ===== localfunctionclose_redis(red) ifnot red thenreturnend local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) ifnot ok then red:close() end end localfunctionget_real_ip() if trusted_proxy_hops <= 0then return ngx.var.remote_addr end local xff = ngx.req.get_headers()["X-Forwarded-For"] ifnot xff thenreturn ngx.var.remote_addr end local ips = {} for ip instring.gmatch(xff, "([^,]+)") do table.insert(ips, ip:match("^%s*(.-)%s*$")) end local target_idx = #ips - trusted_proxy_hops return ips[target_idx] or ngx.var.remote_addr end -- ===== 主流程:fail-open 保护 ===== local redis = require"resty.redis" local client = redis:new() client:set_timeout(redis_connection_timeout) local ok, err = client:connect(redis_host, redis_port) ifnot ok then -- Redis 不可用直接放行,不阻断业务 ngx.log(ngx.WARN, "redis connect failed, fail-open: ", err) return end if client:get_reused_times() == 0then local auth_ok, auth_err = client:auth(redis_auth) ifnot auth_ok then ngx.log(ngx.ERR, "redis auth failed: ", auth_err) close_redis(client) return end end local clientIp = get_real_ip() local incrKey = "limit:count:" .. clientIp local blockKey = "limit:block:" .. clientIp -- 命中黑名单 → 先归还连接再 exit local is_block = client:get(blockKey) iftonumber(is_block) == 1then close_redis(client) return ngx.exit(ngx.HTTP_FORBIDDEN) end -- EVAL 让 incr + expire 原子化,杜绝孤儿 key local count_script = [[ local current = redis.call('INCR', KEYS[1]) if current == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end return current ]] local ip_count, eval_err = client:eval(count_script, 1, incrKey, ip_time_out) ifnot ip_count then ngx.log(ngx.ERR, "eval failed: ", eval_err) close_redis(client) return end iftonumber(ip_count) > ip_max_count then -- SET + EX 一条命令,无需两步 client:set(blockKey, 1, "EX", ip_block_time) end close_redis(client)
local count_script = [[ local current = redis.call('INCR', KEYS[1]) if current == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end return current ]] local ip_count = client:eval(count_script, 1, incrKey, ip_time_out)