一、为什么需要IP黑白名单

在互联网应用中,安全防护是重中之重。想象一下,你的网站就像一家商店,如果不加限制地让所有人进出,难免会遇到一些不怀好意的"顾客"。这时候,IP黑白名单就像是门口的保安,能够根据名单决定谁可以进,谁不能进。

IP白名单就像VIP名单,只有在名单上的IP才能访问;而黑名单则是"不受欢迎名单",名单上的IP会被拒绝访问。这种机制特别适合以下几种场景:

  1. 内部系统只允许公司网络访问
  2. 封禁恶意攻击的IP地址
  3. 限制特定地区的访问
  4. 为合作伙伴提供专属访问通道

传统做法是在防火墙或Web服务器上配置,但每次修改都需要重启服务,不够灵活。而OpenResty提供了更优雅的解决方案。

二、OpenResty简介

OpenResty不是简单的Nginx,它更像是一个"超级Nginx"。它在Nginx基础上集成了LuaJIT,让你可以用Lua脚本扩展Nginx的功能。这就好比给普通的汽车装上了火箭引擎,性能飙升的同时还保持了灵活性。

主要特点包括:

  • 高性能:基于Nginx的事件驱动模型
  • 灵活性:可通过Lua脚本实现复杂逻辑
  • 热加载:配置和规则可动态更新
  • 丰富生态:大量现成的Lua模块可用

特别适合做API网关、Web应用防火墙、流量控制等场景。我们今天要实现的IP黑白名单功能,正是OpenResty的拿手好戏。

三、实现IP黑白名单的三种方式

3.1 基础版:Nginx配置文件方式

最简单的实现方式就是直接修改Nginx配置。虽然这不是最灵活的,但胜在简单直接。

# nginx.conf 示例
http {
    # 白名单配置
    geo $white_list {
        default 0;
        192.168.1.100 1;  # 允许的IP
        192.168.1.101 1;
    }

    # 黑名单配置  
    geo $black_list {
        default 0;
        192.168.1.200 1;  # 拒绝的IP
    }

    server {
        listen 80;
        
        location / {
            # 先检查黑名单
            if ($black_list) {
                return 403;
            }
            
            # 再检查白名单
            if ($white_list = 0) {
                return 403;
            }
            
            # 通过检查的请求
            proxy_pass http://backend;
        }
    }
}

这种方式的优点是配置简单,但缺点也很明显:每次修改都需要重载Nginx配置,不适合频繁变更的场景。

3.2 进阶版:Lua脚本+内存存储

OpenResty的强大之处在于可以用Lua脚本扩展功能。下面我们实现一个更灵活的版本:

-- ip_check.lua
local white_list = {
    ["192.168.1.100"] = true,
    ["192.168.1.101"] = true
}

local black_list = {
    ["192.168.1.200"] = true
}

local function get_client_ip()
    local headers = ngx.req.get_headers()
    local ip = headers["X-Real-IP"] or ngx.var.remote_addr
    return ip
end

local function is_ip_allowed(ip)
    -- 先检查黑名单
    if black_list[ip] then
        return false
    end
    
    -- 如果没有白名单配置,则允许所有
    if not next(white_list) then
        return true
    end
    
    -- 检查白名单
    return white_list[ip]
end

local client_ip = get_client_ip()
if not is_ip_allowed(client_ip) then
    ngx.exit(ngx.HTTP_FORBIDDEN)
end

然后在Nginx配置中引用这个脚本:

location / {
    access_by_lua_file /path/to/ip_check.lua;
    proxy_pass http://backend;
}

这个版本已经灵活多了,但IP列表还是硬编码在脚本里,每次修改需要重新加载脚本。

3.3 终极版:Redis存储+动态更新

为了真正实现动态更新,我们可以把IP列表存在Redis中:

-- redis_ip_check.lua
local redis = require "resty.redis"
local red = redis:new()

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "failed to connect to redis: ", err)
    -- 连接失败时默认放行,可根据业务调整
    return
end

local function get_client_ip()
    local headers = ngx.req.get_headers()
    return headers["X-Real-IP"] or ngx.var.remote_addr
end

local client_ip = get_client_ip()

-- 检查黑名单
local is_black, err = red:sismember("ip:blacklist", client_ip)
if is_black == 1 then
    ngx.exit(ngx.HTTP_FORBIDDEN)
end

-- 检查白名单
local is_white, err = red:sismember("ip:whitelist", client_ip)
-- 如果白名单不为空且IP不在白名单中,则拒绝
local white_count, err = red:scard("ip:whitelist")
if white_count > 0 and is_white ~= 1 then
    ngx.exit(ngx.HTTP_FORBIDDEN)
end

-- 将Redis连接放回连接池
local ok, err = red:set_keepalive(10000, 100)
if not ok then
    ngx.log(ngx.ERR, "failed to set keepalive: ", err)
end

对应的Nginx配置:

location / {
    access_by_lua_file /path/to/redis_ip_check.lua;
    proxy_pass http://backend;
}

现在,我们只需要通过Redis命令就能动态更新IP列表了:

# 添加IP到白名单
redis-cli SADD ip:whitelist 192.168.1.102

# 从白名单移除IP
redis-cli SREM ip:whitelist 192.168.1.101

# 添加IP到黑名单
redis-cli SADD ip:blacklist 192.168.1.201

这种方案完美解决了动态更新的问题,而且Redis的高性能也能保证访问速度。

四、进阶功能与优化

4.1 IP段支持

有时候我们需要支持整个IP段的控制,比如允许192.168.1.0/24网段。我们可以扩展之前的Lua代码:

local function ip_in_cidr(ip, cidr)
    local utils = require "resty.utils"
    return utils.ip_in_cidr(ip, cidr)
end

local function is_ip_allowed(ip)
    -- 检查精确IP匹配的黑名单
    if black_list[ip] then
        return false
    end
    
    -- 检查IP段匹配的黑名单
    for cidr, _ in pairs(black_cidr_list) do
        if ip_in_cidr(ip, cidr) then
            return false
        end
    end
    
    -- 白名单逻辑类似...
end

4.2 性能优化

当IP列表很大时,我们可以做一些优化:

  1. 本地缓存:使用shared dict缓存Redis查询结果
  2. 批量查询:使用Redis的pipeline批量查询
  3. Bloom过滤器:用Bloom过滤器快速判断IP是否可能存在
local function check_ip_with_cache(ip)
    local cache = ngx.shared.ip_cache
    local cached = cache:get(ip)
    
    if cached ~= nil then
        return cached == "1"
    end
    
    -- 查询Redis并更新缓存
    local is_allowed = check_ip_in_redis(ip)
    cache:set(ip, is_allowed and "1" or "0", 60) -- 缓存60秒
    return is_allowed
end

4.3 日志与监控

良好的日志记录对于安全策略至关重要:

local function log_access(ip, is_allowed)
    local logger = require "resty.logger"
    local log = {
        time = ngx.localtime(),
        ip = ip,
        allowed = is_allowed,
        uri = ngx.var.request_uri,
        ua = ngx.var.http_user_agent
    }
    logger:log(log)
end

五、应用场景与最佳实践

5.1 典型应用场景

  1. 内部管理系统:只允许公司内网IP访问
  2. API防护:限制合作伙伴IP调用API
  3. 防爬虫:封禁恶意爬虫IP
  4. 地理限制:允许/禁止特定国家的IP

5.2 技术优缺点

优点:

  • 高性能:OpenResty处理IP检查几乎不影响性能
  • 灵活性:可随时动态更新规则
  • 细粒度:支持精确IP和IP段控制
  • 可扩展:可轻松集成其他安全措施

缺点:

  • 需要维护Redis等外部存储
  • 对于超大规模IP列表需要额外优化
  • 需要熟悉Lua编程

5.3 注意事项

  1. IP伪造:注意前端是否有代理,正确处理X-Forwarded-For
  2. 性能考量:在高并发场景下做好Redis连接管理
  3. 规则顺序:明确黑白名单的优先级
  4. 容灾方案:Redis不可用时要有降级策略
  5. 定期审计:定期审查IP列表的有效性

六、总结

通过OpenResty实现IP黑白名单,我们获得了一个高性能、灵活可扩展的访问控制方案。从最简单的Nginx配置到基于Redis的动态方案,我们可以根据实际需求选择合适的实现方式。

关键点回顾:

  1. OpenResty结合了Nginx的高性能和Lua的灵活性
  2. Redis存储实现了规则的动态更新
  3. 支持IP段和多种优化策略
  4. 适用于多种安全防护场景

未来还可以考虑集成机器学习算法,自动识别和封禁恶意IP,让安全防护更加智能。