Apisix Lua 插件开发说明文档
Apisix lua插件开发说明文档
APISIX主框架代码分析
apisix.core
| 常用模块 | |
|---|---|
| core.schema | 配置文件与配置模板进行对比,看是否满足条件 |
| core.table | 对lua自带table的扩展 |
| core.log | 输出日志在apisix的日志中 |
| core.json | json处理,主要用到了cjson和dkjson。 |
| core.request | 对ngx.req的封装 |
| core.response | 对ngx.resp的封装 |
| core.utils | 封装的工具 |
| core.lrucache | 对resty.lrucache的封装 |
插件内部结构

怎么写lua插件
以一个完整的token校验插件为例
1、定义基本信息(必写)
插件名称、插件的配置信息,包含版本,作用的优先级(插件类似于中间件是堆叠在上面的,优先级指定了谁先执行谁后执行)等。
local core = require("apisix.core")
local jwt = require("resty.jwt")
local ngx = ngx
local pairs = pairs
local plugin_name = "verify-token"
local Contains = {
-- APP请求转发网关时带的token的key
APP_TOKEN_KEY = "XXX-ACCESS-TOKEN",
--PC请求转发网关时带的token的key
PC_TOKEN_KEY = "XXXX-ACCTOKEN",
--请求转发网关时带的token的兜底key
DEFAULT_TOKEN_KEY = "token"
}
local schema = {
type = "object",
properties = {
jwtSecret = { type = "string" },
noverify_uri = { type = "array", minItems = 1,
items = { type = "string" } }
},
required = { "jwtSecret" }
}
local _M = {
version = 0.2,
priority = 3003,
name = plugin_name,
schema = schema,
}
2、检查配置的方法,检测配置是否合规(必写)
function _M.check_schema(conf)
local ok, err = core.schema.check(schema, conf)
if not ok then
return false, err
end
return true
end
这里用到了core里面的方法,local core = require("apisix.core")
3、自定义操作逻辑(按需,自定义,如果多个插件使用,也可以把这些方法放到一个公共模块文件里)
local function matchTable(tbl, pattern)
if next(tbl) == nil then
return false
end
for _, v in ipairs(tbl) do
local match_str = string.gsub(v, '*', '')
if pattern:match('^' .. match_str) then
return true
end
end
return false
end
local function split_string(str, delim)
local result = {}
local sep = string.format("([^%s]+)", delim)
for m in str:gmatch(sep) do
result[#result + 1] = m
end
return result
end
local function verify_token(token, jwtSecret)
local jwt_obj = jwt:verify(jwtSecret, token)
if jwt_obj.verified == false then
return nil, jwt_obj.reason
end
local real_payload = jwt_obj.payload
return real_payload, nil
end
4、校验并重写响应(非必写,自定义响应内容时可用)
除了可以对响应重写,也可以对header,body进行重写
function _M.rewrite(conf, ctx)
local token
local cookie = ngx.req.get_headers()["cookie"]
if cookie then
local cookie_arr = split_string(cookie, "; ")
local cookie_dict = {}
for _, item in pairs(cookie_arr) do
local kv = split_string(item, "=")
local k = kv[1]
local v = kv[2]
cookie_dict[k] = v
end
if cookie_dict[Contains.APP_TOKEN_KEY] then
token = cookie_dict[Contains.APP_TOKEN_KEY]
elseif cookie_dict[Contains.PC_TOKEN_KEY] then
token = cookie_dict[Contains.PC_TOKEN_KEY]
end
else
token = ngx.req.get_headers()[Contains.DEFAULT_TOKEN_KEY]
end
if not token then
return 503, { message = "Missing token in request" }
end
local url_path = ngx.var.request_uri
--如果是路径完全匹配
local match_uri = matchTable(conf.noverify_uri, url_path)
if not match_uri then
local payload, err = verify_token(token, conf.jwtSecret)
--core.log.error(core.json.encode(payload))
if err then
return 503, { message = err }
end
local account = payload["account"]
if not account then
return 503, { message = "token verify fail, account is None" }
end
end
end
return _M
5、修改apisix配置
修改apisix配置文件config.yaml 中的plugins配置项,添加对应的插件名称,注意,插件文件名必须和插件名称一致
6、apisix 重新载入配置
--apisix 重新载入配置
apisix reload
7、写完插件时怎么调试
不建议打印,可以在需要调试的地方写入日志,如 core.log.error(uri .. ', ', has_auth),此时可以打开apisix错误日志对照着看输出。
注意:修改完插件必须执行 apisix reload 插件才能生效。
8、插件调试完成后
--apisix 重新载入配置
apisix reload
--apisix dashboard 插件信息重新生成
curl 127.0.0.1:9090/v1/schema>schema.json
--apisix dashboard 重启
systemctl restart apisix-dashboard
--如果重启完毕错误日志无输出,dashboard上依然找不到插件,就把这三条再执行一遍
ngx_lua 模块提供的指令和API等:
OpenResty Reference:Lua_Nginx_API
https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/
常用自定义模块封装
切割字符串
-- 切割 主要以特殊符号切割
function _M.split_string(str, delim)
local result = {}
local sep = string.format("([^%s]+)", delim)
for m in str:gmatch(sep) do
result[#result + 1] = m
end
return result
end
-- 切割字符串,可以指定长字符串切割,也可以以特殊符号切割,但这种方法不省内存,如需按照特殊符号切割,建议使用上面的方法
function _M.split_word(str, sep)
local t = {}
local i = 0
local j = 1
local z = string.len(sep)
while true do
i = string.find(str, sep, i + 1)
if i == nil then
table.insert(t, string.sub(str, j, -1))
break
end
table.insert(t, string.sub(str, j, i - 1))
j = i + z
end
return t
end
token有效性验证
function _M.verify_token(token, jwtSecret)
local jwt_obj = jwt:verify(jwtSecret, token)
if jwt_obj.verified == false then
return nil, jwt_obj.reason
end
local real_payload = jwt_obj.payload
return real_payload, nil
end
这里用到了jwt,local jwt = require("resty.jwt")
判断table中是否包含某个值或某个键
--判断table中是否包含某个值
function _M.table_vinclude(tab, pattern)
if tab == nil then
return false
end
if next(tab) == nil then
return false
end
for _, v in pairs(tab) do
if string.find(pattern, v) then
--if v == pattern then
return true
end
end
return false
end
--判断table中是否包含某个键
function _M.table_kinclude(tab, pattern)
if tab == nil then
return false
end
if next(tab) == nil then
return false
end
for k, _ in pairs(tab) do
if k == pattern then
return true
end
end
return false
end
redis集群操作
--获取master信息
function _M.redis_query_masterinfo()
local errors = {}
local serv_list = config_map.serv_list
local DEFAULT_MAX_CONNECTION_ATTEMPTS = 3
for i = 1, #serv_list do
local ip = serv_list[i][1]
local port = serv_list[i][2]
local red = redis:new()
local ok, err
red:set_timeout(config_map.timeout)
for k = 1, DEFAULT_MAX_CONNECTION_ATTEMPTS do
ok, err = red:connect(ip, port)
if ok then
break
end
if err then
local err_extra = table.concat({ "host: ", ip, ",port: ", port })
core.log.error("redis Cannot connect, ", err_extra)
return ok, err
end
end
if ok then
local rs, err1 = red:info()
red:set_keepalive(config_map.max_idle_time, config_map.pool_size)
if not rs then
return rs, err1
end
local rs_li = custom_util.split_string(rs, "\n")
local master_info
for _, v in pairs(rs_li) do
local iexist = string.find(v, "master%d:")
if iexist then
master_info = v
end
end
--split_string 按照指定字符串切割,前面有提到该方法
local st = custom_util.split_string(split_word(master_info, ",address=")[2], ",")[1]
local m_ipport = custom_util.split_string(st, ":")
local master_ip = m_ipport[1]
local master_port = m_ipport[2]
return { master_ip, master_port }, nil
end
end
return nil, errors
end
-- redis集群操作 查询指定key
function _M.redis_query_key(key)
local master_info, err = redis_query_masterinfo()
if not master_info then
return nil, err
end
local master_ip = master_info[1]
local master_port = master_info[2]
local red = redis:new()
red:set_timeout(config_map.timeout)
local ok, err = red:connect(master_ip, master_port)
if not ok then
local err_extra = table.concat({ "host: ", master_ip, ",port: ", master_port })
core.log.error("redis Cannot connect, ", err_extra)
return ok, err
end
--密码和选择的桶
local res, err = red:auth(config_map.possword)
if not res then
core.log.error("redis failed to authenticate: ", err)
return res, err
end
red:select(0)
local val = red:get(key)
red:set_keepalive(config_map.max_idle_time, config_map.pool_size)
if val == ngx.null then
err = table.concat({ "redis query key:", key, " is empty" })
return nil, err
end
return val, nil
end
return _M
mysql连接,sql执行
local function mysql_query(sql, dbinfo)
local env = assert(luasql.mysql())
local conn = env:connect(dbinfo.dbname, dbinfo.dbuser, dbinfo.dbpwd, dbinfo.url, dbinfo.port)
local per_dict = {}
local cursor, err = conn:execute(sql)
local row = cursor:fetch({}, 'a')
table.insert(per_dict, row.url)
while row do
-- reusing the table of results
row = cursor:fetch(row, "a")
if row then
table.insert(per_dict, row.url)
end
end
conn:close() --关闭数据库连接
env:close() --关闭数据库环境
return per_dict
end
apisix插件加载流程

apisix 核心流程及工作原理:https://www.cnblogs.com/alioth01/p/19082306

浙公网安备 33010602011771号