强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

12 - Lua 扩展与 OpenResty / Lua & OpenResty

Lua 扩展与 OpenResty / Lua & OpenResty

🟢 基础 / Basics — 什么是 OpenResty?

OpenResty 简介

OpenResty = Nginx + LuaJIT,让 Nginx 具备了编程能力。

传统 Nginx:
配置文件驱动,功能由模块决定,扩展性有限

OpenResty:
Nginx + Lua 脚本 → 可编程的 Web 平台
- 动态路由
- 自定义鉴权
- 实时限流
- 请求/响应改写
- 与 Redis/MySQL 直接交互

安装 OpenResty

# Ubuntu/Debian
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" \
    | sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt update
sudo apt install -y openresty

# CentOS/RHEL
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install -y openresty

# 启动
sudo systemctl start openresty
sudo systemctl enable openresty

第一个 Lua 脚本

# /usr/local/openresty/nginx/conf/nginx.conf

http {
    server {
        listen 80;
        server_name localhost;

        # content_by_lua_block — 直接在 Nginx 中执行 Lua 代码
        location /hello {
            default_type text/html;
            content_by_lua_block {
                ngx.say("<h1>Hello from OpenResty!</h1>")
                ngx.say("<p>Time: ", ngx.localtime(), "</p>")
            }
        }

        # access_by_lua_block — 在 access 阶段执行 Lua
        location /api/ {
            access_by_lua_block {
                local token = ngx.var.arg_token
                if token ~= "mysecret" then
                    ngx.exit(403)
                end
            }
            proxy_pass http://127.0.0.1:3000;
        }
    }
}

Lua 执行阶段 / Execution Phases

server {
    location / {
        # 1. init_worker — Worker 启动时执行一次
        #    用于定时任务、共享字典初始化

        # 2. ssl_certificate_by_lua — SSL 握手阶段
        #    动态证书选择

        # 3. set_by_lua — 设置变量
        set_by_lua_block $greeting {
            return "Hello, " .. (ngx.var.arg_name or "World")
        }

        # 4. rewrite_by_lua — rewrite 阶段
        rewrite_by_lua_block {
            ngx.log(ngx.INFO, "Rewrite phase: ", ngx.var.uri)
        }

        # 5. access_by_lua — access 阶段(鉴权、限流)
        access_by_lua_block {
            if ngx.req.get_method() == "DELETE" then
                ngx.exit(405)
            end
        }

        # 6. content_by_lua — content 阶段(生成响应)
        #    如果使用了 proxy_pass,不需要 content_by_lua

        # 7. header_filter_by_lua — 修改响应头
        header_filter_by_lua_block {
            ngx.header["X-Powered-By"] = "OpenResty"
        }

        # 8. body_filter_by_lua — 修改响应体
        body_filter_by_lua_block {
            local chunk = ngx.arg[1]
            if chunk then
                ngx.arg[1] = chunk:gsub("secret", "****")
            end
        }

        # 9. log_by_lua — 日志阶段
        log_by_lua_block {
            ngx.log(ngx.INFO, "Request completed: ", ngx.var.status)
        }

        proxy_pass http://backend;
    }
}

🟡 进阶 / Intermediate — 实用 Lua 模块

shared_dict(共享字典 — 进程间共享内存)

http {
    # 声明共享内存(所有 Worker 共享)
    lua_shared_dict rate_limit 10m;     # 10MB
    lua_shared_dict api_cache 50m;      # 50MB

    server {
        location /api/ {
            access_by_lua_block {
                local limit_dict = ngx.shared.rate_limit
                local client_ip = ngx.var.remote_addr

                -- 简单的滑动窗口限流
                local key = "rate:" .. client_ip
                local count, err = limit_dict:incr(key, 1, 0, 60)  -- 60 秒窗口

                if count > 100 then    -- 每分钟最多 100 
                    ngx.exit(429)
                    return
                end
            }

            proxy_pass http://backend;
        }

        # 查看限流状态
        location /rate_status {
            content_by_lua_block {
                local dict = ngx.shared.rate_limit
                local keys = dict:get_keys(100)
                ngx.say("Active rate limit keys: ", #keys)
                for _, k in ipairs(keys) do
                    local v = dict:get(k)
                    ngx.say(k, " = ", v)
                end
            }
        }
    }
}

HTTP 请求(ngx.http)

location /weather {
    content_by_lua_block {
        local http = require "resty.http"

        local httpc = http.new()
        httpc:set_timeout(5000)

        local res, err = httpc:request_uri(
            "https://api.weather.com/v1/current",
            {
                method = "GET",
                headers = {
                    ["Authorization"] = "Bearer xxx",
                },
            }
        )

        if not res then
            ngx.log(ngx.ERR, "HTTP request failed: ", err)
            ngx.exit(500)
            return
        end

        ngx.header["Content-Type"] = "application/json"
        ngx.say(res.body)
    }
}

Redis 集成

http {
    lua_shared_dict redis_pool 1m;

    server {
        location /cache/ {
            content_by_lua_block {
                local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeout(1000)

                local ok, err = red:connect("127.0.0.1", 6379)
                if not ok then
                    ngx.log(ngx.ERR, "Redis connect failed: ", err)
                    ngx.exit(500)
                    return
                end

                local key = ngx.var.arg_key or "default"
                local value, err = red:get(key)

                if value == ngx.null then
                    ngx.say("Key not found: ", key)
                else
                    ngx.say("Value: ", value)
                end

                -- 放回连接池(重要!)
                local ok, err = red:set_keepalive(10000, 100)
            }
        }
    }
}

动态路由

http {
    lua_shared_dict routing_table 1m;

    init_by_lua_block {
        -- 初始化路由表
        local routes = ngx.shared.routing_table
        routes:set("/api/users", "http://127.0.0.1:3001")
        routes:set("/api/orders", "http://127.0.0.1:3002")
        routes:set("/api/products", "http://127.0.0.1:3003")
    }

    server {
        location /api/ {
            access_by_lua_block {
                local routes = ngx.shared.routing_table
                local uri = ngx.var.uri

                -- 匹配路由
                for pattern, backend in pairs(routes:get_keys(100)) do
                    if string.find(uri, "^" .. pattern) then
                        ngx.var.target_backend = routes:get(pattern)
                        return
                    end
                end

                ngx.exit(404)
            }

            proxy_pass $target_backend;
        }
    }
}

🔴 高级 / Advanced — 生产级应用

自定义 WAF

http {
    lua_shared_dict waf_rules 10m;
    lua_shared_dict waf_stats 1m;

    init_worker_by_lua_block {
        -- 加载 WAF 规则
        local rules = ngx.shared.waf_rules

        -- SQL 注入关键词
        local sqli_keywords = {
            "union%s+select", "insert%s+into", "drop%s+table",
            "1%s*=%s*1", "or%s+1%s*=%s*1", "'%s+or%s+'"
        }
        for i, kw in ipairs(sqli_keywords) do
            rules:set("sqli:" .. i, kw)
        end

        -- XSS 关键词
        local xss_keywords = {
            "<script", "javascript:", "onerror=", "onload="
        }
        for i, kw in ipairs(xss_keywords) do
            rules:set("xss:" .. i, kw)
        end
    }

    server {
        location / {
            access_by_lua_block {
                local rules = ngx.shared.waf_rules
                local stats = ngx.shared.waf_stats
                local uri = ngx.var.uri
                local args = ngx.var.args or ""
                local body = ""

                -- 读取请求体
                ngx.req.read_body()
                body = ngx.req.get_body_data() or ""

                local request_data = uri .. "?" .. args .. body

                -- 检查 SQL 注入
                local keys = rules:get_keys(100)
                for _, key in ipairs(keys) do
                    local pattern = rules:get(key)
                    if string.find(request_data:lower(), pattern:lower()) then
                        -- 记录统计
                        local count = stats:incr("blocked", 1, 0)
                        ngx.log(ngx.WAF, "Blocked: ", key, " from ", ngx.var.remote_addr)
                        ngx.exit(403)
                        return
                    end
                end
            }

            proxy_pass http://backend;
        }

        -- WAF 统计接口
        location /waf/stats {
            content_by_lua_block {
                local stats = ngx.shared.waf_stats
                ngx.header["Content-Type"] = "application/json"
                ngx.say(cjson.encode({
                    blocked_requests = stats:get("blocked") or 0,
                    total_requests = stats:get("total") or 0
                }))
            }
        }
    }
}

API 网关

http {
    lua_shared_dict jwt_cache 10m;
    lua_shared_dict rate_limit 50m;

    server {
        listen 443 ssl;
        server_name api.example.com;

        location /api/ {
            access_by_lua_block {
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                -- 1. JWT 鉴权
                local auth_header = ngx.req.get_headers()["Authorization"]
                if not auth_header then
                    ngx.status = 401
                    ngx.say(cjson.encode({error = "Missing authorization"}))
                    ngx.exit(401)
                    return
                end

                local token = auth_header:match("Bearer%s+(.+)")
                if not token then
                    ngx.status = 401
                    ngx.say(cjson.encode({error = "Invalid format"}))
                    ngx.exit(401)
                    return
                end

                -- 验证 JWT(简化示例)
                local jwt_obj = jwt:verify("my_secret_key", token)
                if not jwt_obj.verified then
                    ngx.status = 401
                    ngx.say(cjson.encode({error = "Invalid token"}))
                    ngx.exit(401)
                    return
                end

                -- 将用户信息传给后端
                ngx.req.set_header("X-User-ID", jwt_obj.payload.sub)
                ngx.req.set_header("X-User-Role", jwt_obj.payload.role)

                -- 2. 限流
                local key = "rate:" .. jwt_obj.payload.sub
                local limit_dict = ngx.shared.rate_limit
                local count = limit_dict:incr(key, 1, 0, 60)

                if count > 1000 then
                    ngx.status = 429
                    ngx.header["Retry-After"] = "60"
                    ngx.say(cjson.encode({error = "Rate limit exceeded"}))
                    ngx.exit(429)
                    return
                end
            }

            proxy_pass http://backend;
        }

        -- 路由分发
        location /api/v1/users {
            proxy_pass http://user-service;
        }

        location /api/v1/orders {
            proxy_pass http://order-service;
        }

        location /api/v1/products {
            proxy_pass http://product-service;
        }
    }
}

动态上游选择

upstream backend_a { server 10.0.1.10:3000; }
upstream backend_b { server 10.0.2.10:3000; }

server {
    location / {
        set $target_backend "backend_a";

        access_by_lua_block {
            -- 根据请求特征选择上游
            local headers = ngx.req.get_headers()
            local api_version = headers["X-API-Version"]
            local client_tier = headers["X-Client-Tier"]

            if api_version == "v2" then
                ngx.var.target_backend = "backend_b"
            elseif client_tier == "premium" then
                ngx.var.target_backend = "backend_b"
            end
        }

        proxy_pass http://$target_backend;
    }
}

常用 Lua 库

用途安装
lua-resty-httpHTTP 客户端内置
lua-resty-redisRedis 客户端内置
lua-resty-mysqlMySQL 客户端内置
lua-resty-jwtJWT 鉴权opm get SkyLothar/lua-resty-jwt
lua-resty-templateHTML 模板opm get bungle/lua-resty-template
lua-cjsonJSON 编解码内置
lua-resty-lrucacheLRU 缓存内置
lua-resty-coreNginx API 增强内置
# OpenResty 包管理器(opm)
opm get SkyLothar/lua-resty-jwt
opm get ledgetech/lua-resty-http

# 查看已安装的包
opm list

小结 / Summary

层级你需要知道的 / What You Need to Know
🟢 基础OpenResty = Nginx + LuaJIT,content_by_lua_block,执行阶段
🟡 进阶shared_dict 共享内存,Redis/HTTP 集成,动态路由
🔴 高级自定义 WAF,API 网关(JWT + 限流 + 路由),动态上游选择

全书完 / End of Tutorial

恭喜你完成了 Nginx 从入门到精通的全部课程!

Congratulations on completing the entire Nginx tutorial!

推荐继续学习 / Further Reading