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的封装

插件内部结构

image

怎么写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插件加载流程

image

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

posted @ 2025-09-09 17:59  醒日是归时  阅读(66)  评论(0)    收藏  举报