一、Lua代码
0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 -- ################################################################-- 新增:2分钟访问限制功能-- ################################################################local function check_rate_limit()-- 使用共享字典进行限流local limit = ngx.shared.ip_rate_limitif not limit thenngx.log(ngx.ERR, "速率限制共享字典未初始化")return true -- 如果限流不可用,允许访问end-- 获取客户端真实IP(用于限流的key)local limit_ip = ngx.var.remote_addrlocal xff = ngx.var.http_x_forwarded_forif xff thenlocal ips = {}for ip in xff:gsub("%s", ""):gmatch("([^,]+)") dotable.insert(ips, ip)endif #ips > 0 thenlimit_ip = ips[1]endend-- 创建限流keylocal key = "rate_limit:" .. limit_ip .. ":ip_query"local window = 120 -- 2分钟,单位:秒-- 检查是否已经访问过local current = limit:get(key)if current then-- 计算剩余时间local ttl = limit:ttl(key)if ttl and ttl > 0 thenngx.header['Content-Type'] = 'text/plain; charset=utf-8'ngx.header['Retry-After'] = tostring(ttl)--ngx.say("Error: Too many requests, please try again after " .. math.ceil(ttl/60) .. " minutes.")ngx.say("Error: Too many requests, please try again after " .. ttl .. " seconds.")return falseendend-- 设置新的访问记录,过期时间为5分钟local success, err, forcible = limit:set(key, 1, window)if not success thenngx.log(ngx.ERR, "设置速率限制失败: ", err)endreturn trueend-- 执行速率限制检查if not check_rate_limit() thenreturn ngx.exit(429)end-- ################################################################-- 原有代码保持不变-- ################################################################-- 设置响应头为文本格式,字符编码为 UTF-8ngx.header['Content-Type'] = 'text/plain; charset=utf-8'-- 加载依赖库local cjson = require 'cjson'local geo = require 'resty.maxminddb'-- ################################################################-- 函数:获取客户端 IP(支持手动指定、IPv6优先、代理场景)-- ################################################################local function get_client_ip()-- 优先级 1:从 URL 参数获取手动指定的 IP(例如 ?ip=114.114.114.114)local manual_ip = ngx.var.arg_ipif manual_ip and manual_ip ~= "" thenreturn manual_ip, 'manual' -- 返回手动指定的 IP 和来源标记end-- 优先级 2:从 X-Forwarded-For 头获取 IP(支持 IPv6 优先)local xff = ngx.var.http_x_forwarded_forif xff then-- 清理空格并分割 IP 列表local ips = {}for ip in xff:gsub("%s", ""):gmatch("([^,]+)") dotable.insert(ips, ip)end-- 如果有多个IP,优先返回IPv6地址if #ips > 0 then-- 首先尝试找到IPv6地址for _, ip in ipairs(ips) doif ip:find(":") then -- 简单判断是否为IPv6return ip, 'x-forwarded-for-ipv6'endend-- 如果没有IPv6,返回第一个IPv4return ips[1], 'x-forwarded-for-ipv4'endend-- 优先级 3:尝试从 X-Real-IP 头获取local x_real_ip = ngx.var.http_x_real_ipif x_real_ip and x_real_ip ~= "" thenreturn x_real_ip, 'x-real-ip'end-- 优先级 4:使用直接连接的客户端 IP-- 注意:在双栈环境中,remote_addr 可能是 IPv4 或 IPv6return ngx.var.remote_addr, 'direct'end-- ################################################################-- 函数:验证 IP 格式(支持 IPv4/IPv6)-- ################################################################local function validate_ip(ip)-- IPv4 正则(如 192.168.1.1)local ipv4_pattern = "^(%d+)%.(%d+)%.(%d+)%.(%d+)$"-- 检查 IPv4local a, b, c, d = ip:match(ipv4_pattern)if a and b and c and d thena, b, c, d = tonumber(a), tonumber(b), tonumber(c), tonumber(d)if a and b and c and d and a >= 0 and a <= 255 and b >= 0 and b <= 255 and c >= 0 and c <= 255 and d >= 0 and d <= 255 thenreturn true, 'IPv4'endend-- 简化的 IPv6 检查-- 检查是否包含冒号,并且只包含十六进制字符和冒号if ip:find(":") and not ip:find("[^%x:]") then-- 检查压缩格式 (::) 只出现一次if ip:gsub("::", "|"):find("::") thenreturn false, 'invalid'end-- 检查段数local colon_count = 0for _ in ip:gmatch(":") docolon_count = colon_count + 1end-- 标准 IPv6 有 7 个冒号,压缩格式会少一些if colon_count > 7 thenreturn false, 'invalid'end-- 检查每个段的长度local valid = truefor segment in ip:gmatch("[%x]+") doif #segment > 4 thenvalid = falsebreakendendif valid thenreturn true, 'IPv6'endendreturn false, 'invalid'end-- ##############################-- 主逻辑执行-- ##############################-- 获取客户端 IP 及其来源local client_ip, ip_source = get_client_ip()-- 验证 IP 格式有效性local is_valid, ip_type = validate_ip(client_ip)if not is_valid thenngx.say("错误:无效的 IP 地址格式 - ", client_ip)ngx.say("提示:请输入合法的 IPv4(如 114.114.114.114)或 IPv6(如 2001:4860:4860::8888)")return ngx.exit(400)end-- 新增功能:如果指定 only_ipv6 参数,优先返回 IPv6 地址,如果没有则返回 IPv4if ngx.var.arg_only_ipv6 and ngx.var.arg_only_ipv6 ~= "" then-- 如果当前 IP 是 IPv6,直接返回if ip_type == "IPv6" thenngx.say(client_ip)returnelse-- 如果不是 IPv6,检查是否有其他 IPv6 地址可用local xff = ngx.var.http_x_forwarded_forlocal found_ipv6 = nil-- 在 X-Forwarded-For 中查找 IPv6 地址if xff thenfor ip in xff:gsub("%s", ""):gmatch("([^,]+)") dolocal valid, addr_type = validate_ip(ip)if valid and addr_type == "IPv6" thenfound_ipv6 = ipbreakendendend-- 如果找到了 IPv6 地址,返回它if found_ipv6 thenngx.say(found_ipv6)returnelse-- 如果没有 IPv6 地址,返回当前的 IPv4 地址ngx.say(client_ip)returnendendend-- 新增功能:如果指定 only_ipv4 参数,优先返回 IPv4 地址,如果没有则返回 IPv6if ngx.var.arg_only_ipv4 and ngx.var.arg_only_ipv4 ~= "" then-- 如果当前 IP 是 IPv4,直接返回if ip_type == "IPv4" thenngx.say(client_ip)returnelse-- 如果不是 IPv4,检查是否有其他 IPv4 地址可用local xff = ngx.var.http_x_forwarded_forlocal found_ipv4 = nil-- 在 X-Forwarded-For 中查找 IPv4 地址if xff thenfor ip in xff:gsub("%s", ""):gmatch("([^,]+)") dolocal valid, addr_type = validate_ip(ip)if valid and addr_type == "IPv4" thenfound_ipv4 = ipbreakendendend-- 如果找到了 IPv4 地址,返回它if found_ipv4 thenngx.say(found_ipv4)returnelse-- 如果没有 IPv4 地址,返回当前的 IPv6 地址ngx.say(client_ip)returnendendend-- 原有的 only_ip 参数功能(兼容性保留)if ngx.var.arg_only_ip and ngx.var.arg_only_ip ~= "" thenngx.say(client_ip)returnend-- 初始化 GeoIP 数据库if not geo.initted() thenlocal ok, err = geo.init("/usr/share/GeoIP/GeoLite2-City.mmdb")if not ok thenngx.log(ngx.ERR, 'GeoIP 初始化失败: ', err)ngx.say("错误:地理位置服务不可用")return ngx.exit(500)endend-- 查询地理位置信息local res, err = geo.lookup(client_ip)if not res thenngx.log(ngx.ERR, 'IP 查询失败 | IP:', client_ip, ' | 错误:', err)ngx.say("错误:无法查询此 IP 的地理位置")return ngx.exit(500)end-- ##############################-- 输出结果-- ##############################-- 显示 IP 及其来源ngx.say("IP 地址: ", client_ip)ngx.say("IP 类型: ", ip_type)ngx.say("IP 来源: ", ip_source)-- 输出核心地理信息local country = res.country and res.country.names.en or "N/A"local region = res.subdivisions and #res.subdivisions > 0 and res.subdivisions[1].names.en or "N/A"local city = res.city and res.city.names.en or "N/A"local latitude = res.location and res.location.latitude or "N/A"local longitude = res.location and res.location.longitude or "N/A"ngx.say("\n--- 基础地理信息 ---")ngx.say("国家: ", country)ngx.say("地区: ", region)ngx.say("城市: ", city)ngx.say("坐标: ", latitude, ", ", longitude)-- 调试模式输出完整 JSON(通过 ?debug=1 触发)if ngx.var.arg_debug == '1' thenngx.say("\n--- 原始数据 ---")ngx.say(cjson.encode(res))end-- 按需输出节点信息(通过 ?node=location 等参数指定)if ngx.var.arg_node thenlocal node_data = res[ngx.var.arg_node] or {}ngx.say("\n--- 节点查询 [", ngx.var.arg_node, "] ---")ngx.say(cjson.encode(node_data))end
注意事项:
为了让限流功能正常工作,您需要在 OpenResty 配置中添加共享字典
0123456789 http {# 添加共享字典用于IP限流,10MB内存lua_shared_dict ip_rate_limit 10m;server {# 您的其他配置...location /ip {content_by_lua_file /path/to/your/ip.lua;}}}
二、代码接口
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 基础查询 curl "https://note.t4x.org/ip" # 查询指定IP curl "https://note.t4x.org/ip?ip=8.8.8.8" # 只返回IP地址 curl "https://note.t4x.org/ip?only_ip=1" # 优先返回IPv6 curl "https://note.t4x.org/ip?only_ipv6=1" # 优先返回IPv4 curl "https://note.t4x.org/ip?only_ipv4=1" # 调试模式 curl "https://note.t4x.org/ip?debug=1" # 查询特定字段 curl "https://note.t4x.org/ip?node={location,city,country}" # 组合查询 curl "https://note.t4x.org/ip?ip=8.8.8.8&only_ipv4=1&debug=1" |
申明:除非注明Byrd's Blog内容均为原创,未经许可禁止转载!详情请阅读版权申明!
