一、为什么需要IP黑白名单
在互联网应用中,安全防护是重中之重。想象一下,你的网站就像一家商店,如果不加限制地让所有人进出,难免会遇到一些不怀好意的"顾客"。这时候,IP黑白名单就像是门口的保安,能够根据名单决定谁可以进,谁不能进。
IP白名单就像VIP名单,只有在名单上的IP才能访问;而黑名单则是"不受欢迎名单",名单上的IP会被拒绝访问。这种机制特别适合以下几种场景:
- 内部系统只允许公司网络访问
- 封禁恶意攻击的IP地址
- 限制特定地区的访问
- 为合作伙伴提供专属访问通道
传统做法是在防火墙或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列表很大时,我们可以做一些优化:
- 本地缓存:使用shared dict缓存Redis查询结果
- 批量查询:使用Redis的pipeline批量查询
- 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 典型应用场景
- 内部管理系统:只允许公司内网IP访问
- API防护:限制合作伙伴IP调用API
- 防爬虫:封禁恶意爬虫IP
- 地理限制:允许/禁止特定国家的IP
5.2 技术优缺点
优点:
- 高性能:OpenResty处理IP检查几乎不影响性能
- 灵活性:可随时动态更新规则
- 细粒度:支持精确IP和IP段控制
- 可扩展:可轻松集成其他安全措施
缺点:
- 需要维护Redis等外部存储
- 对于超大规模IP列表需要额外优化
- 需要熟悉Lua编程
5.3 注意事项
- IP伪造:注意前端是否有代理,正确处理X-Forwarded-For
- 性能考量:在高并发场景下做好Redis连接管理
- 规则顺序:明确黑白名单的优先级
- 容灾方案:Redis不可用时要有降级策略
- 定期审计:定期审查IP列表的有效性
六、总结
通过OpenResty实现IP黑白名单,我们获得了一个高性能、灵活可扩展的访问控制方案。从最简单的Nginx配置到基于Redis的动态方案,我们可以根据实际需求选择合适的实现方式。
关键点回顾:
- OpenResty结合了Nginx的高性能和Lua的灵活性
- Redis存储实现了规则的动态更新
- 支持IP段和多种优化策略
- 适用于多种安全防护场景
未来还可以考虑集成机器学习算法,自动识别和封禁恶意IP,让安全防护更加智能。
评论