Nginx 结合 Lua:自定义认证授权、流量控制与请求改写实战
Nginx 作为一款高性能的 Web 服务器和反向代理服务器,被广泛应用于各种场景。虽然 Nginx 本身的功能已经非常强大,但在某些特定场景下,我们可能需要对其进行扩展,以满足更复杂的需求。这时,Lua 模块就派上了用场。通过 Lua 脚本,我们可以灵活地扩展 Nginx 的功能,实现自定义的认证授权、流量控制、请求改写等功能。
本文将深入讲解 Nginx 的 Lua 模块,并以实现自定义认证授权、流量控制和请求改写为例,说明如何使用 Lua 脚本扩展 Nginx 功能。同时,还将详细说明如何调试和优化 Lua 脚本的性能。
1. Nginx Lua 模块简介
Nginx Lua 模块,通常指的是 ngx_http_lua_module,它允许我们在 Nginx 配置文件中嵌入 Lua 代码,从而在 Nginx 的各个处理阶段执行 Lua 脚本。这为我们提供了极大的灵活性,可以根据实际需求定制 Nginx 的行为。
1.1 安装和配置
首先,你需要安装 ngx_http_lua_module。具体的安装方式取决于你的操作系统和 Nginx 的安装方式。常见的安装方式包括:
- 使用包管理器: 例如,在 Debian/Ubuntu 上,可以使用
apt-get install nginx-lua-module命令安装。 - 从源码编译: 下载 Nginx 源码,然后添加
--with-http_lua_module编译选项。
安装完成后,需要在 Nginx 的配置文件中启用 Lua 模块。例如,在 nginx.conf 文件中添加以下配置:
http {
lua_package_path '/path/to/lua/scripts/?.lua;;';
lua_package_cpath '/path/to/lua/modules/?.so;;';
...
}
其中,lua_package_path 和 lua_package_cpath 分别指定 Lua 脚本和 C 模块的搜索路径。
1.2 常用指令
ngx_http_lua_module 提供了许多指令,用于在 Nginx 的不同阶段执行 Lua 脚本。常用的指令包括:
init_by_lua: 在 Nginx 启动时执行 Lua 脚本,用于初始化全局变量和函数。init_worker_by_lua: 在每个 worker 进程启动时执行 Lua 脚本,用于初始化 worker 进程相关的变量和函数。set_by_lua: 在设置变量时执行 Lua 脚本,用于动态计算变量的值。access_by_lua: 在访问控制阶段执行 Lua 脚本,用于实现自定义的认证授权。content_by_lua: 在内容生成阶段执行 Lua 脚本,用于生成动态内容。header_filter_by_lua: 在响应头过滤阶段执行 Lua 脚本,用于修改响应头。body_filter_by_lua: 在响应体过滤阶段执行 Lua 脚本,用于修改响应体。log_by_lua: 在日志记录阶段执行 Lua 脚本,用于自定义日志格式。
2. 自定义认证授权
使用 Lua 脚本可以轻松实现自定义的认证授权。例如,我们可以从数据库或 Redis 中读取用户信息,然后根据用户名和密码进行认证。
2.1 示例:基于 Redis 的认证
首先,我们需要安装 Lua Redis 客户端。可以使用 luarocks 命令安装:
luarocks install lua-resty-redis
然后,在 Nginx 配置文件中添加以下配置:
location /protected {
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000) -- 1 second
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 ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
local username = ngx.var.http_username
local password = ngx.var.http_password
if not username or not password then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header["WWW-Authenticate"] = "Basic realm=\"Restricted\""
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local redis_password = red:get("user:" .. username)
if not redis_password then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header["WWW-Authenticate"] = "Basic realm=\"Restricted\""
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
if redis_password ~= password then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header["WWW-Authenticate"] = "Basic realm=\"Restricted\""
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 认证成功
red:close()
}
...
}
这段代码首先连接 Redis 服务器,然后从 Redis 中读取用户名对应的密码。如果用户名或密码不正确,则返回 401 Unauthorized 错误。否则,认证成功,允许访问 /protected 路径。
注意: 上面的代码只是一个简单的示例,实际应用中需要考虑更多的安全因素,例如密码加密存储、防止暴力破解等。
2.2 获取用户名和密码
在上面的示例中,我们使用了 ngx.var.http_username 和 ngx.var.http_password 来获取用户名和密码。这意味着我们需要通过 HTTP Basic Authentication 来传递用户名和密码。当然,我们也可以使用其他方式,例如从 Cookie 或请求体中获取。
3. 流量控制
使用 Lua 脚本可以实现各种流量控制策略,例如限制单个 IP 的访问频率、限制总的并发连接数等。
3.1 示例:限制单个 IP 的访问频率
我们可以使用 resty.limit.req 模块来实现限制单个 IP 的访问频率。首先,需要安装该模块:
luarocks install resty-limit-req
然后,在 Nginx 配置文件中添加以下配置:
http {
lua_shared_dict my_limit_req_store 10m;
...
location /api {
limit_req zone=my_limit_req_zone burst=5 nodelay;
access_by_lua_block {
local limit_req = require "resty.limit.req"
local limiter, err = limit_req.new("my_limit_req_store", 1, 0.2)
if not limiter then
ngx.log(ngx.ERR, "failed to create limiter: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
local key = ngx.var.remote_addr
local delay, err = limiter:take(key, 1)
if not delay then
ngx.log(ngx.ERR, "failed to take token: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
if delay > 0 then
ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
}
...
}
}
这段代码使用了 lua_shared_dict 指令创建了一个共享内存区域,用于存储 IP 地址和访问次数。limit_req 指令指定了限制的 zone 和 burst 大小。access_by_lua_block 中的代码使用 resty.limit.req 模块来实现具体的限流逻辑。如果某个 IP 的访问频率超过了限制,则返回 429 Too Many Requests 错误。
3.2 其他流量控制策略
除了限制单个 IP 的访问频率,我们还可以使用 Lua 脚本实现其他流量控制策略,例如:
- 限制总的并发连接数: 可以使用
resty.limit.conn模块来实现。 - 根据用户 ID 限制访问频率: 可以从 Cookie 或请求头中获取用户 ID,然后使用
resty.limit.req模块来限制每个用户的访问频率。 - 根据请求内容限制访问频率: 可以根据请求的 URL、参数等内容来限制访问频率。
4. 请求改写
使用 Lua 脚本可以实现各种请求改写操作,例如修改请求头、修改请求体、重定向请求等。
4.1 示例:修改请求头
在 Nginx 配置文件中添加以下配置:
location /api {
header_filter_by_lua_block {
ngx.header["X-Custom-Header"] = "Custom Value"
}
...
}
这段代码在响应头过滤阶段执行 Lua 脚本,添加了一个名为 X-Custom-Header 的自定义响应头,值为 Custom Value。
4.2 示例:重定向请求
在 Nginx 配置文件中添加以下配置:
location /old-api {
content_by_lua_block {
ngx.redirect("/new-api", ngx.HTTP_MOVED_PERMANENTLY)
}
}
这段代码在内容生成阶段执行 Lua 脚本,将访问 /old-api 的请求重定向到 /new-api,并返回 301 Moved Permanently 状态码。
4.3 其他请求改写操作
除了修改请求头和重定向请求,我们还可以使用 Lua 脚本实现其他请求改写操作,例如:
- 修改请求体: 可以使用
ngx.req.get_body_data和ngx.req.set_body_data函数来获取和修改请求体。 - 添加或删除请求参数: 可以使用
ngx.req.get_uri_args和ngx.req.set_uri_args函数来获取和修改请求参数。 - 修改请求方法: 可以使用
ngx.req.set_method函数来修改请求方法。
5. 调试和优化 Lua 脚本
调试和优化 Lua 脚本对于提高 Nginx 的性能至关重要。
5.1 调试 Lua 脚本
使用
ngx.log函数: 可以使用ngx.log函数将调试信息输出到 Nginx 的错误日志中。例如:ngx.log(ngx.INFO, "username: ", username, ", password: ", password)使用
print函数: 如果你使用了 OpenResty,可以使用print函数将调试信息输出到控制台。使用调试工具: 可以使用 Lua 的调试工具,例如 ZeroBrane Studio,来调试 Lua 脚本。
5.2 优化 Lua 脚本
- 避免阻塞操作: 尽量避免在 Lua 脚本中使用阻塞操作,例如数据库查询、网络请求等。可以使用 Nginx 提供的非阻塞 API,例如
ngx.socket.tcp,或者使用 Lua coroutine 来实现异步操作。 - 使用 LuaJIT: LuaJIT 是一个高性能的 Lua 解释器,可以显著提高 Lua 脚本的性能。OpenResty 默认使用 LuaJIT。
- 缓存数据: 对于一些不经常变化的数据,可以使用
lua_shared_dict指令将其缓存到共享内存区域中,以避免重复读取。 - 减少内存分配: 频繁的内存分配会影响性能。可以尽量重用对象,避免创建过多的临时变量。
- 使用 Lua 代码分析器: 可以使用 Lua 代码分析器,例如 luacheck,来检查代码中的潜在问题,并进行优化。
6. 总结
本文深入讲解了 Nginx 的 Lua 模块,并以实现自定义认证授权、流量控制和请求改写为例,说明如何使用 Lua 脚本扩展 Nginx 功能。同时,还详细说明了如何调试和优化 Lua 脚本的性能。希望本文能够帮助你更好地理解和使用 Nginx Lua 模块,从而构建更强大、更灵活的 Web 应用。
通过 Nginx 结合 Lua,我们可以实现许多强大的功能,例如:
- Web 应用防火墙 (WAF): 可以使用 Lua 脚本来检测和防御各种 Web 攻击,例如 SQL 注入、XSS 攻击等。
- API 网关: 可以使用 Lua 脚本来实现 API 的认证授权、流量控制、请求改写、监控等功能。
- 动态内容生成: 可以使用 Lua 脚本来生成各种动态内容,例如 JSON 数据、HTML 页面等。
Nginx Lua 模块为我们提供了极大的灵活性,可以根据实际需求定制 Nginx 的行为。只要你掌握了 Lua 语言和 Nginx Lua 模块,就可以充分发挥 Nginx 的潜力,构建出满足各种需求的 Web 应用。