五、11个指令介绍
OpenResty 有 11 个 *_by_lua指令,它们和 NGINX 阶段的关系如下图所示
其中, init_by_lua 只会在 Master 进程被创建时执行,init_worker_by_lua 只会在每个 Worker 进程被创建时执行。其他的 *_by_lua 指令则是由终端请求触发,会被反复执行。
所以在 init_by_lua 阶段,我们可以预先加载 Lua 模块和公共的只读数据,这样可以利用操作系统的 COW(copy on write)特性,来节省一些内存。
对于业务代码来说,其实大部分的操作都可以在 content_by_lua 里面完成,但我更推荐的做法,是根据不同的功能来进行拆分,比如下面这样:
-
set_by_lua*
: 流程分支处理判断变量初始化 -
rewrite_by_lua*
: 转发、重定向、缓存等功能(例如特定请求代理到外网) -
access_by_lua*
: IP 准入、接口权限等情况集中处理(例如配合 iptable 完成简单防火墙) -
content_by_lua*
: 内容生成 -
header_filter_by_lua*
: 响应头部过滤处理(例如添加头部信息) -
body_filter_by_lua*
: 响应体过滤处理(例如完成应答内容统一成大写) -
log_by_lua*
: 会话完成后本地异步完成日志记录(日志可以记录在本地,还可以同步到其他机器)
我们假设,你对外提供了很多明文 API,现在需要增加自定义的加密和解密逻辑。那么请问,你需要修改所有 API 的代码吗?
# 明文协议版本
location /mixed {
content_by_lua '...'; # 处理请求
}
当然不用。事实上,利用阶段的特性,我们只需要简单地在 access 阶段解密,在 body filter 阶段加密就可以了,原来 content 阶段的代码是不用做任何修改的:
# 加密协议版本
location /mixed {
access_by_lua '...'; # 请求体解密
content_by_lua '...'; # 处理请求,不需要关心通信协议
body_filter_by_lua '...'; # 应答体加密
}
真实代码示例:
下面是一个真实项目中权限校验的某个Lua脚本
nginx.conf
location ~ /paopao/(game/callback/recharge) {
access_by_lua_file lua/util/commonVerifyNotUid.lua;
proxy_pass http://paopao;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Remote-Addr $remote_addr;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
}
upstream paopao{
server xxx.xxx.xxx.xxx:port max_fails=1 fail_timeout=8s weight=5;
}
commonVerifyNotUid.lua
module("util.commonVerify", package.seeall)
local cjson = require"cjson"
local config = require"config"
local securityver = require"util.securityver"
local args = ngx.req.get_uri_args()
local headers = ngx.req.get_headers()
--安全验证 必须参数
local requestId = headers.requestId --流水号
local appid = headers.appId -- appid
local c = headers.c -- 平台
local vn = headers.vn -- 版本
local ua = headers.ua --ua
local sign = headers.sign -- 签名
local u = headers.u -- 渠道号
local subAppId = headers.subAppId -- 子AppId
ngx.req.read_body()
local postargs = ngx.req.get_post_args()
if not u or u == true or u == "" then
ngx.log(ngx.ERR, "u header is nil")
ngx.print(cjson.encode({ result = 1 , msg ='gateway parameter absent: u'}))
ngx.exit(200)
return
end
if not requestId or requestId == true or requestId == "" then
ngx.log(ngx.ERR, "requestId header is nil")
ngx.print(cjson.encode({ result = 1 , msg ='gateway parameter absent: requestId'}))
ngx.exit(200)
return
end
if not appid or appid == true or appid == "" then
-- 同上
end
if not c or c == true or c == "" then
-- 同上
end
if not vn or vn == true or vn == "" then
-- 同上
end
if not ua or ua == true or ua == "" then
-- 同上
end
if not sign or sign == true or sign == "" then
-- 同上
end
-- 黑名单imei
local f = io.open("/data/paopao/paopao_gw/lua/black_list", "r")
local black_list = f:read("*all")
f:close()
--ngx.log(ngx.INFO, black_list)
black_list = loadstring("return " .. black_list)()
function getimei(s)
for k,v in string.gmatch(s, "(%w+)=(%w+)") do
if(k == 'imei')
then
return v
end
end
end
function ifblock(imei)
for k, v in ipairs(black_list) do
if v == imei then
return true
end
end
return false
end
imei_str = getimei(ua)
if ifblock(imei_str) then
ngx.print(cjson.encode({ result = 1, msg = 'black list'}))
ngx.log(ngx.INFO, "黑名单")
ngx.exit(200)
return
end
if ngx.var.args then
ngx.var.args = ngx.var.args .. '&pv=' .. config.PV[c] .. '&v=' .. vn .. '&appId=' .. appid .. '&u=' .. u
else
ngx.var.args = 'pv=' .. config.PV[c] .. '&v=' .. vn .. '&appId=' .. appid .. '&u=' .. u
end
if subAppId ~= nil then
ngx.var.args = ngx.var.args .. '&subAppId=' .. subAppId
end
六、API介绍
OpenResty 是基于 NGINX 的 Web 服务器,但它与 NGINX 却有本质的不同:NGINX 由静态的配置文件驱动,而 OpenResty 是由 Lua API 驱动的,所以能提供更多的灵活性和可编程性。
OpenResty 的 API 主要分为下面几个大类:
-
处理请求和响应;
-
SSL 相关;
-
shared dict;
-
cosocket;
-
处理四层流量;
-
process 和 worker;
-
获取 NGINX 变量和配置;
-
字符串、时间、编解码等通用功能。
penResty 的 API 不仅仅存在于 lua-nginx-module 项目中,也存在于 lua-resty-core 项目中,比如 ngx.ssl、ngx.base64、ngx.errlog、ngx.process、ngx.re.split、ngx.resp.add_header、ngx.balancer、ngx.semaphore、ngx.ocsp 这些 API 。
而对于不在 lua-nginx-module 项目中的 API,你需要单独 require 才能使用。举个例子,比如你想使用 split 这个字符串分割函数,就需要按照下面的方法来调用:
$ resty -e 'local ngx_re = require "ngx.re"
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
print(res)
'
下面我们介绍几个常用的API:
1.获取uri参数
获取一个 uri 有两个方法:ngx.req.get_uri_args
、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' -d 'c=3&d=4'
[GET ] key:b v:2
[GET ] key:a v:1
[POST] key:d v:4
[POST] key:c v:3
从以上输入结果可以看出:前者来自 uri 请求参数,而后者来自 post 请求内容。
我们拿真实的案例来分析一下:
nginx文件中需要拦截的路径加入下面代码
access_by_lua_file lua/util/commonVerify.lua;
以下为Lua文件中部分代码
lua会获取到get和post的请求参数,然后会对参数加密得到一个sign,同时对客户端传递过来的sign进行比对,如果失败,则返回错误提示。
commonVerify.lua
local securityver = require"util.securityver"
local args = ngx.req.get_uri_args()
local postargs = ngx.req.get_post_args()
--安全验证
local status1, res = pcall(securityver.Verify, args, postargs)
if not status1 then
ngx.log(ngx.ERR, "error in function securityver.Verify. status is nil")
ngx.print(cjson.encode({ result = 3 , msg ='gateway verify: inner exception' }))
ngx.exit(200)
return
end
seurityver.lua
module("util.securityver", package.seeall)
package.path = package.path .. ';/data/uxin/http_gw_v3/lua/protobuf/?.lua'
package.cpath = package.cpath .. ';/data/uxin/http_gw_v3/lua/protobuf/?.so'
require 'ssid_pb'
local config = require"config"
local string = require("string")
local commonutil = require"util.commonutil"
local map = { d = 0, e = 1, y = 2, b = 3, i = 4, p = 5, v = 6, k = 7, z = 8, o = 9 }
local function sha1(src)
local resty_sha1 = require"resty.sha1"
local sha1 = resty_sha1:new()
if not sha1 then
return ""
end
local ok = sha1:update(src)
if not ok then
return ""
end
local digest = sha1:final() -- binary digest
local str = require"resty.string"
return str.to_hex(digest)
end
local function VerifySign(args,postargs)
local appid = ngx.req.get_headers().appid
local ua = ngx.req.get_headers().ua
local requestId = ngx.req.get_headers().requestId
local vn = ngx.req.get_headers().vn
local paramsTable = {}
local sign = ngx.req.get_headers().sign
local signSrc = ""
ngx.log(ngx.ERR, "----vn :" .. vn)
if vn >= "1.0.0" then
--table.merge(args, postargs)
for k,v in pairs(postargs) do
ngx.log(ngx.ERR, "args k" .. k)
ngx.log(ngx.ERR, "args type: " .. type(v))
if type(v) ~= 'table' then
ngx.log(ngx.ERR, "args v" .. v)
args[k] = v
end
end
end
ngx.log(ngx.ERR, "----vn :" .. vn)
if args then
for k, v in pairs(args) do
if k ~= "sign" and v ~= true and v ~= "" then
ngx.log(ngx.ERR, "args k" .. k)
table.insert(paramsTable, k)
end
end
table.sort(paramsTable, function(a, b)
return string.lower(a) < string.lower(b)
end)
for i = 1, #(paramsTable) do
ngx.log(ngx.ERR, "arg:" .. paramsTable[i] .."--- " .. args[paramsTable[i]])
signSrc = signSrc .. args[paramsTable[i]]
end
end
--local SignKey = config.SIGNKEY
ngx.log(ngx.INFO, "----------")
ngx.log(ngx.INFO, config.SIGN_KEY[appid])
ngx.log(ngx.INFO, "----------")
local SignKey = config.SIGN_KEY[appid]
local signStr = signSrc .. requestId .. ua .. appid .. SignKey
local sing1 = sha1(signStr)
--ngx.log(ngx.ERR, "----bad sign, signStr:" .. signStr)
--ngx.log(ngx.ERR, "----bad sign, source sign:" .. sign)
--ngx.log(ngx.ERR, "----bad sign, native sign:" .. sing1)
if sing1 ~= sign then
ngx.log(ngx.ERR, "bad sign, signStr:" .. signStr)
ngx.log(ngx.ERR, "bad sign, source sign:" .. sign)
ngx.log(ngx.ERR, "bad sign, native sign:" .. sing1)
return false
end
return true
end
-- 防止用户重复请求的方法,客户端会传一个requestId,为随机生成的字符串
local function VerifyRequestId(args)
--make sure account+sn is different in one hour, then sign is different
local uid = args.uid
if not uid then
args = ngx.req.get_uri_args()
uid = args.uid
end
local snSet = ngx.shared.SnSet
local sn = ngx.req.get_headers().requestId
if uid then
sn = uid .. sn
end
--ngx.log(ngx.ERR, "duplicate requestId" .. sn)
local success, err, forcible = snSet:add(sn, 1, 5 * 60)
if not success and err == "exists" then
ngx.log(ngx.ERR, "duplicate requestId" .. sn)
return false
-- return true
end
return true
end
local function VerifyParam(args)
return true
end
function Verify(args,postargs)
-- 注销
--return true
if not VerifyParam(args) then
ngx.log(ngx.ERR, "VerifyParam is error")
return false
end
if not VerifyRequestId(args) then
ngx.log(ngx.ERR, "verify requestId is error")
return false
end
return VerifySign(args,postargs)
end
config.lua
我们将常量单独存放到一个lua类中保管
module("config", package.seeall)
SIGNKEY = "xxxxxxxxxxxxxxxxxxxxxxx"
2.获取请求body
在 Nginx 的典型应用场景中,几乎都是只读取 HTTP 头即可,例如负载均衡、正反向代理等场景。但是对于 API Server 或者 Web Application ,对 body 可以说就比较敏感了。由于 OpenResty 基于 Nginx ,所以天然的对请求 body 的读取细节与其他成熟 Web 框架有些不同。
我们先来构造最简单的一个请求,POST 一个名字给服务端,服务端应答一个 “Hello xxx”。
http {
server {
listen 80;
location /test {
content_by_lua_block {
local data = ngx.req.get_body_data()
ngx.say("hello ", data)
}
}
}
}
测试结果:
➜ ~ curl 127.0.0.1/test -d jack
hello nil
大家可以看到 data 部分获取为空,如果你熟悉其他 web 开发框架,估计立刻就觉得 OpenResty 弱爆了。查阅一下官方 wiki 我们很快知道,原来我们还需要添加指令 lua_need_request_body 。究其原因,主要是 Nginx 诞生之初主要是为了解决负载均衡情况,而这种情况,是不需要读取 body 就可以决定负载策略的,所以这个点对于 API Server 和 Web Application 开发的同学有点怪。
参看下面例子:
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)
}
}
}
}
再次测试,符合我们预期:
➜ ~ curl 127.0.0.1/test -d jack
hello jack
3.日志输出
OpenResty 的标准日志输出原句为 ngx.log(log_level, ...)
,几乎可以在任何 ngx_lua 阶段进行日志的输出。
我们用泡泡真实项目中的日志模拟一下sign签名校验错误的日志输入
nginx中的日志配置如下:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "-"' '"upstream_addr:' '$upstream_addr' '-response_time:' '$upstream_response_time"';
server{
access_log logs/httpserver.access.log main;
error_log logs/httpserver.error.log error;
}
httpserver.error.log日志如下:
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:95: bad sign, signStr:50492122955321ssadaadf1200rko753*qpsd5vbalt#$%^19plmo!@&kn, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:96: bad sign, source sign:asadfdfa3124321asd, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:97: bad sign, native sign:7b5a29291b78e9d7432e0a2bca39b94a1c8857e1, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] commonVerify.lua:103: error in function securityver.Verify. res is nil, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gUid=122955&gameId=50492 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
4.发起http请求
OpenResty 最主要的应用场景之一是 API Server,有别于传统 Nginx 的代理转发应用场景,API Server 中心内部有各种复杂的交易流程和判断逻辑。
① 引用 resty.http
库资源,它来自 github https://github.com/pintsized/lua-resty-http。
② 参考 resty-http
官方 wiki 说明,我们可以知道 request_uri 函数完成了连接池、HTTP 请求等一系列动作。
下面的代码是我们利用http库自定义的一个简单的发起http请求的方法:
module("util.httplib", package.seeall)
local os = require("os")
function geturl(purl)
local http = require"resty.http"
local hc = http:new()
local startTime = os.time()
local ok, code, headers, status, body = hc:request{
url = purl,
timeout = 4000,
method = "GET"
}
if not ok then
ngx.log(ngx.ERR, "WARN: " .. purl .. " code: " .. (code or "nil") .. " status: " .. (status or "nil"))
end
if (os.time() - startTime) >= 4 then
ngx.log(ngx.ERR, "WARN: call remote server timeout. the url: " .. purl)
end
return ok, code, headers, status, body
end
function posturl(purl, pbody)
local http = require"resty.http"
local hc = http:new()
local startTime = os.time()
local ok, code, headers, status, body = hc:request{
url = purl,
timeout = 4000,
method = "POST",
body = pbody,
-- add post content-type and cookie
headers = { ["Content-Type"] = "application/x-www-form-urlencoded" },
}
if not ok then
ngx.log(ngx.ERR, "WARN: " .. purl .. " pbody: " .. pbody .. " code: " .. (code or "nil") .. " status: " .. (status or "nil"))
end
if (os.time() - startTime) >= 4 then
ngx.log(ngx.ERR, "WARN: call remote server timeout. the url: " .. purl .. pbody)
end
return ok, code, headers, status, body
end
七、OpenResty缓存
ngx.shared.DICT
我们在nginx.conf中配置
lua_shared_dict SnSet 300m;
lua_shared_dict Threshold 300m;
我们可以用 shared dict 来共享数据,这些数据可以在多个 worker 之间共享。内部使用的 LRU 算法(最近最少使用)来判断缓存是否在内存占满时被清除。
在上面的多处代码中我们都使用了配置文件所定义好的全局变量。
它对外提供了 20 多个 Lua API,不过所有的这些 API 都是原子操作,你不用担心多个 worker 和高并发的情况下的竞争问题。
这些 API 都有官方详细的文档,使用的时候可以查阅:shared_dict
继续看 shared dict 的 API,这些 API 可以分为下面三个大类,也就是字典读写类、队列操作类和管理类这三种。
1.字典读写类
首先来看字典读写类。在最初的版本中,只有字典读写类的 API,它们也是共享字典最常用的功能。下面是一个最简单的示例:
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
dict:set("Tom", 56)
print(dict:get("Tom"))'
除了 set 外,OpenResty 还提供了 safe_set、add、safe_add、replace 这四种写入的方法。这里safe 前缀的含义是,在内存占满的情况下,不根据 LRU 淘汰旧的数据,而是写入失败并返回 no memory 的错误信息。
除了 get 外,OpenResty 还提供了 get_stale 的读取数据的方法,相比 get 方法,它多了一个过期数据的返回值:
value, flags, stale = ngx.shared.DICT:get_stale(key)
还可以调用 delete 方法来删除指定的 key,它和 set(key, nil) 是等价的。
2.管理类
用户申请了 100M 的空间作为 shared dict,那么这 100M 是否够用呢?里面存放了多少 key?具体是哪些 key 呢?
首先是 get_keys(max_count?),它默认也只返回前 1024 个 key;如果你把 max_count 设置为 0,那就返回所有 key。
然后是 capacity 和 free_space,这两个 API 都属于 lua-resty-core 仓库,所以需要你 require 后才能使用:
require "resty.core.shdict"
local cats = ngx.shared.cats
local capacity_bytes = cats:capacity()
local free_page_bytes = cats:free_space()
它们分别返回的,是共享内存的大小(也就是 lua_shared_dict 中配置的大小)和空闲页的字节数。因为 shared dict 是按照页来分配的,即使 free_space 返回为 0,在已经分配的页面中也可能存在空间,所以它的返回值并不能代表共享内存实际被占用的情况。
3.队列操作类
-
lpush/rpush,表示在队列两端增加元素;
-
lpop/rpop,表示在队列两端弹出元素;
-
llen,表示返回队列的元素数量。
下面是代码示例:
=== TEST 1: lpush & lpop
--- http_config
lua_shared_dict dogs 1m;
--- config
location = /test {
content_by_lua_block {
local dogs = ngx.shared.dogs
local len, err = dogs:lpush("foo", "bar")
if len then
ngx.say("push success")
else
ngx.say("push err: ", err)
end
local val, err = dogs:llen("foo")
ngx.say(val, " ", err)
local val, err = dogs:lpop("foo")
ngx.say(val, " ", err)
local val, err = dogs:llen("foo")
ngx.say(val, " ", err)
local val, err = dogs:lpop("foo")
ngx.say(val, " ", err)
}
}
--- request
GET /test
--- response_body
push success
1 nil
bar nil
0 nil
nil nil
--- no_error_log
[error]
八、典型的应用场景
-
在 Lua 中混合处理不同 Nginx 模块输出(proxy, drizzle, postgres, Redis, memcached 等)。
-
在请求真正到达上游服务之前,Lua 中处理复杂的准入控制和安全检查。
-
比较随意的控制应答头(通过 Lua)。
-
从外部存储中获取后端信息,并用这些信息来实时选择哪一个后端来完成业务访问。
-
在内容 handler 中随意编写复杂的 web 应用,同步编写异步访问后端数据库和其他存储。
-
在 rewrite 阶段,通过 Lua 完成非常复杂的处理。
-
在 Nginx 子查询、location 调用中,通过 Lua 实现高级缓存机制。
-
对外暴露强劲的 Lua 语言,允许使用各种 Nginx 模块,自由拼合没有任何限制。该模块的脚本有充分的灵活性,同时提供的性能水平与本地 C 语言程序无论是在 CPU 时间方面以及内存占用差距非常小。所有这些都要求 LuaJIT 2.x 是启用的。其他脚本语言实现通常很难满足这一性能水平。