OpenResty最佳实践笔记(5)

转自OpenResty最佳实践

windows 文档

配置环境变量

创建工作目录, 启动 nginx

创建独立的nginx工作目录 your/path

conf 目录下创建一个文本文件作为配置文件,命名为 nginx.conf

文件内容如下:


worker_processes  1;        #nginx worker 数量
error_log logs/error.log;   #指定错误日志文件路径
events {
    worker_connections 1024;
}

http {
    server {
        #监听端口,若你的6699端口已经被占用,则需要修改
        listen 6699;
        location / {
            default_type text/html;

            content_by_lua_block {
                ngx.say("HelloWorld")
            }
        }
    }
}

启动 nginx

start nginx

或者指定 自己的 nginx目录

start nginx -p 'your/path'

访问 http://localhost:6699/

HelloWorld

关闭

nginx -s stop

# 优雅 退出
nginx -s quit

重启

命令在不停止服务器的情况下重新加载 nginx 配置
nginx -s reload 


指示 nginx 重新打开所有 日志文件
nginx -s reopen

resty script 调试

resty -e "ngx.say('Hello, OpenResty!')"
Hello, OpenResty!

location 配合

nginx 世界的 location 是异常强大的,毕竟 nginx 的主要应用场景是在负载均衡API server, 在不同 serverlocation 之间跳转更是家常便饭。

利用不同 location 的功能组合,我们可以完成内部调用流水线方式跳转外部重定向

内部调用 internal

数据库内部公共函数的统一接口,可以把它们放到统一的 location

通常情况下,为了保护这些内部接口,都会把这些接口设置为 internal 这么做的最主要好处就是可以让这个内部接口相对独立,不受外界干扰

 location = /sum {
            # 只允许内部调用
            internal;

            # 这里做了一个求和运算只是一个例子,可以在这里完成一些数据库、
            # 缓存服务器的操作,达到基础模块和业务逻辑分离目的
            content_by_lua_block {
                local args = ngx.req.get_uri_args()
                ngx.say(tonumber(args.a) + tonumber(args.b))
            }
        }

        location = /app/test {
            content_by_lua_block {
                local res = ngx.location.capture(
                                "/sum", {args={a=3, b=8}}
                 )
                ngx.say("status:", res.status, " response:", res.body)
            }
        }

http://localhost:6699/sum    外部不能访问

    404 Not Found
    openresty/1.19.9.1

http://localhost:6699/app/test
status:200 response:11

并行请求,串行请求实例

ngx.location.capture_multi vs ngx.location.capture

ngx.location.capture_multi( {
    {"/sum", {args={a=3, b=8}}},
    {"/subduction", {args={a=3, b=8}}}
})

ngx.location.capture(
    "/sum", {args={a=3, b=8}}
) 

location = /sum {
    internal;
    content_by_lua_block {
        ngx.sleep(0.1)
        local args = ngx.req.get_uri_args()
        ngx.print(tonumber(args.a) + tonumber(args.b))
    }
}

location = /subduction {
    internal;
    content_by_lua_block {
        ngx.sleep(0.1)
        local args = ngx.req.get_uri_args()
        ngx.print(tonumber(args.a) - tonumber(args.b))
    }
}

location = /app/test_parallels {
    content_by_lua_block {
        local start_time = ngx.now()
        local res1, res2 = ngx.location.capture_multi( {
                        {"/sum", {args={a=3, b=8}}},
                        {"/subduction", {args={a=3, b=8}}}
                    })
        ngx.say("status:", res1.status, " response:", res1.body)
        ngx.say("status:", res2.status, " response:", res2.body)
        ngx.say("time used:", ngx.now() - start_time)
    }
}

location = /app/test_queue {
    content_by_lua_block {
        local start_time = ngx.now()
        local res1 = ngx.location.capture(
                        "/sum", {args={a=3, b=8}}
                    )
        local res2 = ngx.location.capture(
                        "/subduction", {args={a=3, b=8}}
                    )
        ngx.say("status:", res1.status, " response:", res1.body)
        ngx.say("status:", res2.status, " response:", res2.body)
        ngx.say("time used:", ngx.now() - start_time)
    }
}


➜  ~ curl 127.0.0.1/app/test_parallels
status:200 response:11
status:200 response:-5
time used:0.10099983215332

➜  ~ curl 127.0.0.1/app/test_queue
status:200 response:11
status:200 response:-5
time used:0.20199990272522

利用 ngx.location.capture_multi 函数,直接完成了两个子请求并行执行。 当两个请求没有相互依赖,这种方法可以极大提高查询效率。

流水线方式跳转 ngx.exec

仿工厂的流水线模式,逐层过滤、处理

location ~ ^/static/([-_a-zA-Z0-9/]+).jpg {
    set $image_name $1;
    content_by_lua_block {
        ngx.exec("/download_internal/images/"
                .. ngx.var.image_name .. ".jpg");
    };
}

location /download_internal {
    internal;
    # 这里还可以有其他统一的 download 下载设置,例如限速等
    alias ../download;
}



这里的两个 location 更像是流水线上工人之间的协作关系。
第一环节的工人对完成自己处理部分后,直接交给第二环节处理人(实际上可以有更多环节),它们之间的数据流是定向的


ngx.exec 方法与 ngx.redirect 完全不同,ngx.exec纯粹的内部跳转并且没有引入任何额外 HTTP 信号

外部重定向 ngx.redirect



location = /foo {
    content_by_lua_block {
        ngx.say([[I am foo]])
    }
}

location = / {
    rewrite_by_lua_block {
        return ngx.redirect('/foo');
    }
}



使用浏览器访问页面 http://127.0.0.1 就可以发现浏览器会自动跳转到 http://127.0.0.1/foo

外部重定向是可以跨域名的。

从 A 网站跳转到 B 网站是绝对允许的。

在 CDN 场景的大量下载应用中,一般分为调度存储两个重要环节。

调度就是通过根据请求方 IP 、下载文件等信息寻找最近、最快节点,应答跳转给请求方完成下载。

获取请求参数 url参数

如何正确获取、设置 uri 参数

获取请求 uri 参数

ngx.req.get_uri_args ( uri 请求参数) + ngx.req.get_post_args( post 请求内容)

ngx.req.read_body() -- 解析 body 参数之前一定要先读取 body
local arg = ngx.req.get_post_args()
server {
   listen    80;
   server_name  localhost;

   location /print_param {
       content_by_lua_block {
           local arg = ngx.req.get_uri_args()
           for k,v in pairs(arg) do
               ngx.say("[GET ] key:", k, " v:", v)
           end

           ngx.req.read_body() -- 解析 body 参数之前一定要先读取 body
           local arg = ngx.req.get_post_args()
           for k,v in pairs(arg) do
               ngx.say("[POST] key:", k, " v:", v)
           end
       }
   }
}


➜  ~  curl '127.0.0.1/print_param?a=1&b=2%26' -d 'c=3&d=4%26'
[GET ] key:b v:2&
[GET ] key:a v:1
[POST] key:d v:4&
[POST] key:c v:3

传递请求 uri 参数

URI 内容传递过程中 调用 ngx.encode_args 进行规则转义



location /test {
   content_by_lua_block {
       local res = ngx.location.capture(
                '/print_param',
                {
                   method = ngx.HTTP_POST,
                   args = ngx.encode_args({a = 1, b = '2&'}),
                   body = ngx.encode_args({c = 3, d = '4&'})
               }
            )
       ngx.say(res.body)
   }
}



# 不调用 ngx.encode_args

local res = ngx.location.capture('/print_param',
         {
            method = ngx.HTTP_POST,
            args = 'a=1&b=2%26',  -- 注意这里的 %26 ,代表的是 & 字符
            body = 'c=3&d=4%26'
        }
     )
ngx.say(res.body)





获取请求body get_body_data

全局设置 lua_need_request_body 或者 调用 ngx.req.read_body()

Nginx 的典型应用场景,几乎都是只读取 HTTP 头即可,例如负载均衡正反向代理等场景。

但是对于 API Server 或者 Web Application ,对 body 可以说就比较敏感了

Nginx 诞生之初主要是为了解决负载均衡情况,而这种情况,是不需要读取 body 就可以决定负载策略

默认是不读取 body

所以需要 设置lua_need_request_body

OpenResty 基于 Nginx ,所以天然的对请求 body 的读取细节与其他成熟 Web 框架有些不同
http {
    server {
        listen    80;

        # 默认读取 body
        lua_need_request_body on;

        location /test {
            content_by_lua_block {
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}
# 或者 

http {
    server {
        listen    80;

        location /test {
            content_by_lua_block {
                ngx.req.read_body()
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}

输出响应体

HTTP响应报文分为三个部分:

响应行

响应头

响应体

对于 HTTP 响应体的输出,在 OpenResty 中调用 ngx.sayngx.print 即可

ngx.say 与 ngx.print 均为异步输出

优雅处理响应体过大的输出

  • 输出内容本身体积很大

    例如超过 2G 的文件下载

利用HTTP 1.1 特性 CHUNKED 编码, 把一个大的响应体拆分成多个小的应答体,分批、有节制的响应给请求方

location /test {
    content_by_lua_block {
        -- ngx.var.limit_rate = 1024*1024
        local file, err = io.open(ngx.config.prefix() .. "data.db","r")
        if not file then
            ngx.log(ngx.ERR, "open file error:", err)
            ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
        end

        local data
        while true do
            data = file:read(1024)
            if nil == data then
                break
            end
            ngx.print(data)
            ngx.flush(true)
        end
        file:close()
    }
}

  • 输出内容本身是由各种碎片拼凑的,碎片数量庞大

    例如应答数据是某地区所有人的姓名

利用 ngx.print 的特性,它的输入参数可以是单个或多个字符串参数,也可以是 table 对象

local table = {
     "hello, ",
     {"world: ", true, " or ", false,
         {": ", nil}}
 }
 ngx.print(table)

也就是说当有非常多碎片数据时,没有必要一定连接成字符串后再进行输出。
完全可以直接存放在 table 中,用数组的方式把这些碎片数据统一起来,
直接调用 ngx.print(table) 即可。这种方式效率更高,并且更容易被优化

日志输出

标准日志输出

OpenResty 的标准日志输出原句为 ngx.log(log_level, ...) ,几乎可以在任何 ngx_lua 阶段进行日志的输出。

OpenResty 里面的 print 语句是 INFO 级别

#user  nobody;
worker_processes  1;

error_log  logs/error.log error;    # 日志级别
#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    server {
        listen    80;
        location / {
            content_by_lua_block {
                local num = 55
                local str = "string"
                local obj
                ngx.log(ngx.ERR, "num:", num)
                ngx.log(ngx.INFO, " string:", str) -- 不会打印 (日志级别为 error)
                print([[i am print]])              -- 不会打印 (日志级别为 error)
                ngx.log(ngx.ERR, " object:", obj)
            }
        }
    }
}


Nginx 的日志级别,请看下表:

ngx.STDERR     -- 标准输出
ngx.EMERG      -- 紧急报错
ngx.ALERT      -- 报警
ngx.CRIT       -- 严重,系统故障,触发运维告警系统
ngx.ERR        -- 错误,业务不可恢复性错误
ngx.WARN       -- 告警,业务中可忽略错误
ngx.NOTICE     -- 提醒,业务比较重要信息
ngx.INFO       -- 信息,业务琐碎日志信息,包含不同情况判断等
ngx.DEBUG      -- 调试

对于应用开发,一般使用 ngx.INFO 到 ngx.CRIT 就够了

生产中错误日志开启到 error 级别就够了

网络日志输出

如果日志需要归集, 对时效性要求比较高 lua-resty-logger-socket 的目标是替代 Nginx 标准的 ngx_http_log_module非阻塞 IO 方式推送 access log 到远程服务器上

lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";

    server {
        location / {
            log_by_lua_block {
                local logger = require "resty.logger.socket"
                if not logger.initted() then
                    local ok, err = logger.init{
                        host = 'xxx',
                        port = 1234,
                        flush_limit = 1234,
                        drop_limit = 5678,
                    }
                    if not ok then
                        ngx.log(ngx.ERR, "failed to initialize the logger: ",
                                err)
                        return
                    end
                end

                -- construct the custom access log message in
                -- the Lua variable "msg"

                local bytes, err = logger.log(msg)
                if err then
                    ngx.log(ngx.ERR, "failed to log message: ", err)
                    return
                end
            }
        }
    }

lua-resty-logger-socket 几个好处:

  • 基于 cosocket 非阻塞 IO 实现
  • 日志累计到一定量,集体提交,增加网络传输利用率
  • 短时间的网络抖动,自动容错
  • 日志累计到一定量,如果没有传输完毕,直接丢弃
  • 日志传输过程完全不落地,没有任何磁盘 IO 消耗

简单的API server框架


worker_processes  1;        #nginx worker 数量
error_log logs/error.log;   #指定错误日志文件路径
events {
    worker_connections 1024;
}

http {
    # 设置默认 lua 搜索路径,添加 lua 路径
    # 此处写相对路径时,对启动 nginx 的路径有要求,必须在 nginx 目录下启动,require 找不到
    # comm.param 绝对路径当然也没问题,但是不可移植,因此应使用变量 $prefix 或
    # ${prefix},OR 会替换为 nginx 的 prefix path。

    # lua_package_path 'lua/?.lua;/blah/?.lua;;';
    lua_package_path '$prefix/lua/?.lua;/blah/?.lua;;';

    # 这里设置为 off,是为了避免每次修改之后都要重新 reload 的麻烦。
    # 在生产环境上务必确保 lua_code_cache 设置成 on。
    lua_code_cache off;

    server {
        listen 80;

        # 在代码路径中使用nginx变量
        # 注意: nginx var 的变量一定要谨慎,否则将会带来非常大的风险
        location ~ ^/api/([-_a-zA-Z0-9/]+) {
            # 准入阶段完成参数验证
            access_by_lua_file  lua/access_check.lua;

            #内容生成阶段
            content_by_lua_file lua/$1.lua;
        }
    }
}


其他文件内容:

--========== {$prefix}/lua/addition.lua
local args = ngx.req.get_uri_args()
ngx.say(args.a + args.b)

--========== {$prefix}/lua/subtraction.lua
local args = ngx.req.get_uri_args()
ngx.say(args.a - args.b)

--========== {$prefix}/lua/multiplication.lua
local args = ngx.req.get_uri_args()
ngx.say(args.a * args.b)

--========== {$prefix}/lua/division.lua
local args = ngx.req.get_uri_args()
ngx.say(args.a / args.b)


--========== {$prefix}/lua/comm/param.lua
local _M = {}

-- 对输入参数逐个进行校验,只要有一个不是数字类型,则返回 false
function _M.is_number(...)
    local arg = {...}

    local num
    for _,v in ipairs(arg) do
        num = tonumber(v)
        if nil == num then
            return false
        end
    end

    return true
end

return _M

--========== {$prefix}/lua/access_check.lua
local param= require("comm.param")
local args = ngx.req.get_uri_args()

if not args.a or not args.b or not param.is_number(args.a, args.b) then
    ngx.exit(ngx.HTTP_BAD_REQUEST)
    return
end

整体目录关系:

.
├── conf
│   ├── nginx.conf
├── logs
│   ├── error.log
│   └── nginx.pid
├── lua
│   ├── access_check.lua
│   ├── addition.lua
│   ├── subtraction.lua
│   ├── multiplication.lua
│   ├── division.lua
│   └── comm
│       └── param.lua
└── sbin
    └── nginx

使用Nginx内置绑定变量

OpenResty中引用 Nginx内置绑定变量 ngx.var.VARIABLE

名称 说明
$arg_name 请求中的name参数
$args 请求中的参数
$binary_remote_addr 远程地址的二进制表示
$body_bytes_sent 已发送的消息体字节数
$content_length HTTP请求信息里的”Content-Length”
$content_type 请求信息里的”Content-Type”
$document_root 针对当前请求的根路径设置值
$document_uri 与$uri相同; 比如 /test2/test.php
$host 请求信息中的”Host”,如果请求中没有Host行,则等于设置的服务器名
$hostname 机器名使用 gethostname系统调用的值
$http_cookie cookie 信息
$http_referer 引用地址
$http_user_agent 客户端代理信息
$http_via 最后一个访问服务器的Ip地址。
$http_x_forwarded_for 相当于网络访问路径
$is_args 如果请求行带有参数,返回“?”,否则返回空字符串
$limit_rate 对连接速率的限制
$nginx_version 当前运行的nginx版本号
$pid worker进程的PID
$query_string 与$args相同
$realpath_root 按root指令或alias指令算出的当前请求的绝对路径。其中的符号链接都会解析成真是文件路径
$remote_addr 客户端IP地址
$remote_port 客户端端口号
$remote_user 客户端用户名,认证用
$request 用户请求
$request_body 这个变量(0.7.58+)包含请求的主要信息。在使用proxy_pass或fastcgi_pass指令的location中比较有意义
$request_body_file 客户端请求主体信息的临时文件名
$request_completion 如果请求成功,设为”OK”;如果请求未完成或者不是一系列请求中最后一部分则设为空
$request_filename 当前请求的文件路径名,比如/opt/nginx/www/test.php
$request_method 请求的方法,比如”GET”、”POST”等
$request_uri 请求的URI,带参数
$scheme 所用的协议,比如http或者是https
$server_addr 服务器地址,如果没有用listen指明服务器地址,使用这个变量将发起一次系统调用以取得地址(造成资源浪费)
$server_name 请求到达的服务器名
$server_port 请求到达的服务器端口号
$server_protocol 请求的协议版本,”HTTP/1.0”或”HTTP/1.1”
$uri 请求的URI,可能和最初的值有不同,比如经过重定向之类的
   location /sum {


         # 使用access阶段完成准入阶段处理
            access_by_lua_block {
                local black_ips = {["127.0.0.1"]=true}

                local ip = ngx.var.remote_addr
                if true == black_ips[ip] then
                    ngx.exit(ngx.HTTP_FORBIDDEN)
                end
            }

            #处理业务
           content_by_lua_block {
                local a = tonumber(ngx.var.arg_a) or 0
                local b = tonumber(ngx.var.arg_b) or 0
                ngx.say("sum: ", a + b )
            }
        }


大多数内容都是不允许写入修改的

对于可写的变量中的limit_rate,值得一提,它能完成传输速率限制,并且它的影响是单个请求级别


 location /download {
            access_by_lua_block {
                ngx.var.limit_rate = 1000
            };
        }

子查询(子请求)

Nginx 子请求是一种非常强有力的方式,它可以发起非阻塞的内部请求访问目标 location

目标 location 可以是配置文件中其他文件目录,或 任何 其他 nginx C 模块,包括 ngx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle,甚至 ngx_lua 自身等等 。

子请求只是模拟 HTTP 接口的形式没有 额外的 HTTP/TCP 流量,也 没有 IPC (进程间通信) 调用所有工作在 内部高效地在 C 语言级别完成

注意:

子请求与 HTTP 301/302 重定向指令 (通过 ngx.redirect) 完全不同

内部重定向 ((通过 ngx.exec) 完全不同

在发起子请求前,用户程序应总是读取完整的 HTTP 请求体 (通过调用 ngx.req.read_body 或设置 lua_need_request_body 指令为 on).

该 API 方法(ngx.location.capture_multi 也一样)总是缓冲整个请求体到内存中。 因此,当需要处理一个大的子请求响应,用户程序应使用 cosockets 进行流式处理

注:

ngx.location.capturengx.location.capture_multi 指令无法抓取包含以下指令的 location:

add_before_body, add_after_body, auth_request, echo_location, echo_location_async, echo_subrequest, 或 echo_subrequest_async 。

 location /foo {
     content_by_lua_block {
         res = ngx.location.capture("/bar")
     }
 }
 location /bar {
     echo_location /blah;
 }
 location /blah {
     echo "Success!";
 }

curl -i http://example.com/foo
不会按照预期工作。

不同阶段共享变量

OpenResty 的体系中

可以通过共享内存的方式完成不同工作进程的数据共享

可以通过 Lua 模块方式完成单个进程内不同阶段的数据共享

典型的例子,就是在 log 阶段记录一些请求的特殊变量

ngx.ctx 表 就是为了解决这类问题而设计的

location /test {
     rewrite_by_lua_block {
         ngx.ctx.foo = 76
     }
     access_by_lua_block {
         ngx.ctx.foo = ngx.ctx.foo + 3
     }
     content_by_lua_block {
         ngx.say(ngx.ctx.foo)
     }
 }

http://localhost/test # 79
单个进程内不同请求(不同阶段 rewrite_by_lua_block, access_by_lua_block, content_by_lua_block)  的数据共享

首先 ngx.ctx 是一个,所以我们可以对他添加、修改。用来存储基于请求的 Lua 环境数据,其生存周期当前请求相同 (类似 Nginx 变量)

一个最重要的特性:单个请求内的 rewrite (重写),access (访问),和 content (内容) 等各处理阶段是保持一致的

每个请求,包括子请求,都有一份自己的 ngx.ctx 表


location /sub {
             content_by_lua_block {
                 ngx.say("sub pre: ", ngx.ctx.blah)
                 ngx.ctx.blah = 32
                 ngx.say("sub post: ", ngx.ctx.blah)
             }
         }

location /main {
     content_by_lua_block {
         ngx.ctx.blah = 73
         ngx.say("main pre: ", ngx.ctx.blah)
         local res = ngx.location.capture("/sub")
         ngx.print(res.body)
         ngx.say("main post: ", ngx.ctx.blah)
     }
}

http://localhost/sub
sub pre: nil
sub post: 32



http://localhost/main

main pre: 73
sub pre: nil
sub post: 32
main post: 73


由于 ngx.ctx 保存的是指定请求资源,所以这个变量是不能直接共享给其他请求使用的

注意:

ngx.ctx 表查询需要相对昂贵的元方法调用,这比通过用户自己的函数参数直接传递基于请求的数据要慢得多

不要为了节约用户函数参数而滥用此 API,因为它可能对性能有明显影响。

防止SQL注入

SQL 注入,就是通过把 SQL 命令插入到 Web 表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令

 利用现有应用程序,将(恶意)的 SQL 命令注入到后台数据库引擎执行的能力,
 它可以通过在 Web 表单中输入(恶意)SQL 语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行 SQL 语句
 
 
 比如先前的很多影视网站泄露 VIP 会员密码大多就是通过 Web 表单递交查询字符暴出的,
 这类表单特别容易受到 SQL 注入式攻击

SQL 注入例子

location /test {
    content_by_lua_block {
        local mysql = require "resty.mysql"
        local db, err = mysql:new()
        if not db then
            ngx.say("failed to instantiate mysql: ", err)
            return
        end

        db:set_timeout(1000) -- 1 sec

        local ok, err, errno, sqlstate = db:connect{
            host = "127.0.0.1",
            port = 3306,
            database = "ngx_test",
            user = "ngx_test",
            password = "ngx_test",
            max_packet_size = 1024 * 1024 }

        if not ok then
            ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate)
            return
        end

        ngx.say("connected to mysql.")

        local res, err, errno, sqlstate =
            db:query("drop table if exists cats")
        if not res then
            ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
            return
        end

        res, err, errno, sqlstate =
            db:query("create table cats "
                     .. "(id serial primary key, "
                     .. "name varchar(5))")
        if not res then
            ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
            return
        end

        ngx.say("table cats created.")

        res, err, errno, sqlstate =
            db:query("insert into cats (name) "
                     .. "values (\'Bob\'),(\'\'),(null)")
        if not res then
            ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
            return
        end

        ngx.say(res.affected_rows, " rows inserted into table cats ",
                "(last insert id: ", res.insert_id, ")")

        -- 这里有 SQL 注入(后面的 drop 操作)
        local req_id = [[1'; drop table cats;--]]
        res, err, errno, sqlstate =
            db:query(string.format([[select * from cats where id = '%s']], req_id))
        if not res then
            ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
            return
        end

        local cjson = require "cjson"
        ngx.say("result: ", cjson.encode(res))

        -- 再次查询,table 被删
        res, err, errno, sqlstate =
            db:query([[select * from cats where id = 1]])
        if not res then
            ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
            return
        end

        db:set_keepalive(10000, 100)
    }
}



connected to mysql.
table cats created.
3 rows inserted into table cats (last insert id: 1)
result: [{"name":"Bob","id":"1"}]
bad result: failed to send query: cannot send query in the current context: 2: nil: nil.

规避 SQL注入

OpenResty 中,需要对输入参数进行一层过滤即可

ndk.set_var.set_quote_sql_str


-- for MySQL
local req_id = [[1'; drop table cats;--]]
res, err, errno, sqlstate =
    db:query(string.format([[select * from cats where id = %s]],
    ndk.set_var.set_quote_sql_str(req_id)))
if not res then
    ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
    return
end

发起新的HTTP请求

OpenResty 最主要的应用场景之一是 API Server, 需要高效的与其他 HTTP Server 调用 (HTTP 接口调用方法)

利用 proxy_pass

ngx.location.capture+proxy_pass




http {
    upstream md5_server{
        server 127.0.0.1:81;        # ①   上游访问地址清单(可以按需配置不同的权重规则)
        keepalive 20;               # ②   上游访问长连接,是否开启长连接,对整体性能影响比较大
    }
    
    # # 业务系统

    server {
        listen    80;

        location /test {
            content_by_lua_block {
                ngx.req.read_body()
                local args, err = ngx.req.get_uri_args()

                -- ③ 接口访问通过 ngx.location.capture 的子查询方式发起
                local res = ngx.location.capture('/spe_md5',
                    {
                        method = ngx.HTTP_POST,
                        body = args.data
                    }
                )

                if 200 ~= res.status then
                    ngx.exit(res.status)
                end

                if args.key == res.body then
                    ngx.say("valid request")
                else
                    ngx.say("invalid request")
                end
            }
        }
        # 调用
        location /spe_md5 {
            proxy_pass http://md5_server;   # ④ 由于 ngx.location.capture 方式只能是 nginx 自身的子查询,需要借助 proxy_pass 发出 HTTP 连接信号
            #For HTTP, the proxy_http_version directive should be set to “1.1” and the “Connection” 
            #header field should be cleared.(from:http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive)
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }
    
    
    # “盐” md5 计算 服务
    server {
        listen    81;           # ⑤ 公共 API 输出服务

        location /spe_md5 {
            content_by_lua_block {
                ngx.req.read_body()
                local data = ngx.req.get_body_data()
                ngx.print(ngx.md5(data .. "*&^%$#$^&kjtrKUYG"))
            }
        }
    }
}


利用 cosocket [推荐]

resty.http

http {
    server {
        listen    80;

        location /test {
            content_by_lua_block {
                ngx.req.read_body()
                local args, err = ngx.req.get_uri_args()

                local http = require "resty.http"   -- ① 引用 resty.http 库资源 [引用 resty.http 库资源 https://github.com/ledgetech/lua-resty-http]
                local httpc = http.new()
                local res, err = httpc:request_uri( -- ② 参考 resty-http 官方 wiki 说明,我们可以知道 request_uri 函数完成了连接池、HTTP 请求等一系列动作
                    "http://127.0.0.1:81/spe_md5",
                        {
                        method = "POST",
                        body = args.data,
                      }
                )

                if 200 ~= res.status then
                    ngx.exit(res.status)
                end

                if args.key == res.body then
                    ngx.say("valid request")
                else
                    ngx.say("invalid request")
                end
            }
        }
    }

    server {
        listen    81;

        location /spe_md5 {
            content_by_lua_block {
                ngx.req.read_body()
                local data = ngx.req.get_body_data()
                ngx.print(ngx.md5(data .. "*&^%$#$^&kjtrKUYG"))
            }
        }
    }
}


如果你的内部请求比较少,使用 ngx.location.capture+proxy_pass 的方式还没什么问题。

如果你的请求数量比较多,或者需要频繁的修改上游地址,那么 resty.http就更适合你

Buy me a 肥仔水!