跳到主要内容
版本:3.9.x

开发自定义 Lua 插件

自定义 Lua 插件可以在请求/响应生命周期的不同阶段执行自定义逻辑,从而扩展 API7 网关功能。本指南将带你创建一个简单的 Lua 插件,在 API7 网关中注册该插件,并将其应用到路由。

前置条件​

  • 具备 Lua 编程语言基础知识。
  • 熟悉 OpenResty 和 NGINX 请求生命周期。
  • 能够访问 API7 网关实例及其控制台。
  • 从控制台获取 Token。

插件结构​

Lua 插件是一个导出 table 的模块,其中包含配置 Schema、优先级和阶段处理函数。

标准字段​

每个插件都应定义以下字段:

  • name:插件的唯一名称。
  • version:插件版本。
  • priority:决定同一阶段内插件的执行顺序,优先级越高越先执行。
  • schema:定义插件配置参数的 JSON Schema。

执行阶段​

API7 网关先完成路由匹配,然后执行该路由上配置的插件。路由匹配和插件的 rewrite 阶段都发生在 NGINX 的 access 处理过程中。即使请求未匹配任何路由,全局规则仍可能执行,随后网关返回 404。

之后,API7 网关按以下顺序执行插件阶段:

  1. rewrite:路由匹配后的第一个插件阶段。可以修改发往上游的请求,例如 URI 或请求头,但不会使 APISIX 重新匹配路由。
  2. access:主要处理阶段,例如身份认证和限流。
  3. before_proxy:在 access 阶段之后、请求代理到上游之前执行。
  4. header_filter:修改响应头。
  5. body_filter:修改响应体。
  6. log:响应发送给客户端后记录日志。

分步开发​

1. 创建插件文件​

为插件新建 Lua 文件。本示例使用名为 custom-header 的插件。

2. 定义 Schema 和优先级​

使用 apisix.core 根据 JSON Schema 校验插件配置。

local core = require("apisix.core")

local schema = {
type = "object",
properties = {
header_name = {
type = "string",
default = "X-Custom-Lua"
},
header_value = {
type = "string",
default = "loaded"
}
}
}

local plugin_name = "custom-header"

local _M = {
version = 0.1,
priority = 1500,
name = plugin_name,
schema = schema,
}

function _M.check_schema(conf)
return core.schema.check(schema, conf)
end

3. 实现阶段处理函数​

在适当阶段实现逻辑。若要在请求到达上游前添加请求头,请使用 access 阶段。

function _M.access(conf, ctx)
core.request.set_header(ctx, conf.header_name, conf.header_value)
end

4. 完整插件示例​

以下是 custom-header 插件的完整代码:

local core = require("apisix.core")

local schema = {
type = "object",
properties = {
header_name = {
type = "string",
default = "X-Custom-Lua"
},
header_value = {
type = "string",
default = "loaded"
}
}
}

local plugin_name = "custom-header"

local _M = {
version = 0.1,
priority = 1500,
name = plugin_name,
schema = schema,
}

function _M.check_schema(conf)
return core.schema.check(schema, conf)
end

function _M.access(conf, ctx)
core.request.set_header(ctx, conf.header_name, conf.header_value)
end

return _M

在 API7 网关中注册插件​

在 API7 网关中,自定义插件由控制面管理。准备好 Lua 源文件后,通过控制台上传并部署,使它出现在所选网关组的路由、服务、消费者或全局规则的插件选择器中。

1. 在控制台中添加插件​

在控制台中进入 Gateway Settings,打开 Custom Plugin 选项卡,然后点击 Add Custom Plugin。

上传 Lua 源文件并提供所需插件元数据:

  • Plugin Source Code File:上传 custom-header.lua。
  • Plugin Description:简要说明插件功能。
  • Plugin Documentation Link:可选,填写插件文档链接。
  • Plugin Author:标明插件所有者。
  • Deployed Gateway Group:选择应当可以使用该插件的网关组。为降低上线风险,首次只选择测试网关组,详见跨网关组发布。

文件上传成功后,API7 网关会解析插件,并在添加前于对话框显示检测到的 name 和 version。

添加插件后,可以在已部署网关组的路由和服务 Add Plugin 对话框中选择它。

有关产品操作流程,请参阅自定义插件。

2. 确认插件可用​

在一个已部署的网关组中打开路由或服务,确认 Add Plugin 对话框中出现 custom-header。

添加依赖库​

通过控制台上传的自定义插件会注册为独立的插件模块。控制面将其源代码分发给网关并直接加载到运行时,不会把文件写入网关的 Lua 模块搜索路径。因此,另一个插件无法可靠地通过 require() 加载已上传的自定义插件。不要把共享库当作自定义插件上传供其他插件 require:即使一开始似乎可用,网关重启后也可能因依赖插件没有先加载而失效。

有些插件会共享辅助代码,或依赖第三方 Lua 模块或原生 C 库。请将依赖安装到网关的包路径,并像往常一样在插件中 require。

将依赖放到 Lua 或 C 路径​

网关通过两类搜索路径解析模块:

  • Lua 模块通过 lua_package_path 解析。可使用 require("myorg.utils") 加载 <dir>/myorg/utils.lua。
  • 原生 Lua/C 模块通过 lua_package_cpath 解析。可使用 require("myorg.native") 加载 <dir>/myorg/native.so。

可以使用网关内置路径或自定义目录:

  • 内置路径:将文件放到网关内置搜索路径(例如 /usr/local/apisix/),并与 require 的模块名保持一致。

  • 额外路径(推荐):将代码与运行时分离,并在数据面 config.yaml 中添加自定义目录:

    apisix:
    extra_lua_path: "/data/lua/?.lua" # 扩展 lua_package_path
    extra_lua_cpath: "/data/clib/?.so" # 扩展 lua_package_cpath

    详情请参阅API7 网关数据面配置参考和配置文件。

备注

通过 LuaJIT FFI 的 ffi.load() 加载的原生库由系统动态链接器解析,而不是 lua_package_cpath。请将这类库放到系统库路径(例如 /usr/local/lib),或在网关上设置 LD_LIBRARY_PATH。

让网关能够访问文件​

网关启动前,每个网关节点上都必须存在依赖文件:

  • Kubernetes / Helm:使用卷(如 ConfigMap 或持久卷)把文件挂载到网关 Pod,并让 extra_lua_path / extra_lua_cpath 指向挂载路径。
  • Docker / Docker Compose:将目录绑定挂载到网关容器。

已上传的自定义插件由控制面动态分发,而包路径中的库在网关 Worker 启动时加载。添加或修改这些库后,请重新加载或重启网关使变更生效。

示例​

将共享辅助函数放入网关路径上的普通 Lua 模块,例如 /data/lua/myorg/base.lua:

local _M = {}

function _M.verify(token)
-- 多个插件共用的逻辑
return token ~= nil
end

return _M

将目录添加到数据面配置:

apisix:
extra_lua_path: "/data/lua/?.lua"

然后在任意自定义插件中 require:

local core = require("apisix.core")
local base = require("myorg.base")

-- ...

function _M.access(conf, ctx)
if not base.verify(core.request.header(ctx, "Authorization")) then
return 401
end
end

由于模块位于包路径,require("myorg.base") 在每个 Worker 上的解析方式相同,网关重启后仍然有效。

测试插件​

插件源代码在网关节点可用且已在 API7 网关中注册后,可以使用 Admin API 或 ADC 在特定路由上启用它。

  1. 创建启用该插件的路由:
curl -k "https://localhost:7443/apisix/admin/services/custom-header-service?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "custom-header-service",
"upstream": {
"type": "roundrobin",
"scheme": "http",
"nodes": [
{
"host": "httpbin.org",
"port": 80,
"weight": 100
}
]
}
}'

curl -k "https://localhost:7443/apisix/admin/routes/custom-header-route?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "custom-header-route",
"methods": ["GET"],
"paths": ["/get"],
"service_id": "custom-header-service",
"plugins": {
"custom-header": {
"header_name": "X-Trace-ID",
"header_value": "docs-test"
}
}
}'
  1. 发送请求并验证上游响应体中的请求头:
curl -i "http://localhost:9080/get"

httpbin 响应的 headers 中应包含 X-Trace-ID: docs-test,这表明插件在代理前修改了请求。

如果路由创建后代理立即返回 404 Route Not Found,请等待几秒后重试。

跨网关组发布​

自定义插件按网关组部署。每个网关组都是隔离的配置范围:部署到一个组的插件不能供另一组的路由或服务使用。借助这种隔离,可以先在非生产组验证插件,再让它影响生产流量。

推荐的发布流程如下:

  1. 部署到测试网关组。添加插件时,在 Deployed Gateway Group 中只选择测试或预发布网关组。插件此时仅对该组可用。
  2. 验证。按照测试插件在测试组的路由上启用插件并确认行为符合预期。生产组尚无该插件,因此迭代期间不受影响。
  3. 发布到生产环境。验证完成后,编辑该自定义插件,将生产网关组添加到 Deployed Gateway Group。控制面会立即把插件分发到新选中的网关组。

如需调整部署范围,在 Gateway Settings 中打开 Custom Plugin 选项卡,打开插件,更新 Deployed Gateway Group,然后保存。

请注意:

  • 更新会同时作用于所有已部署的网关组。上传新版本会同时更新该插件所在的全部网关组,不能按组分别保存不同版本。因此,上述先测试后上线的流程适用于新插件。若要验证已在生产环境运行的插件的改动,应先用不同且唯一的名称上传一个仅部署到测试组的插件;验证通过后,再更新生产插件。
  • 移除网关组会撤回插件。如果该组中的路由仍引用插件,这些路由可能失效。确认没有依赖后再移除网关组。
  • 插件名称在控制面全局唯一。自定义插件是一个具名对象,可部署到多个网关组,并非每组各有一份独立副本。发布过程中应保持名称唯一且稳定。

调试提示​

  • 检查错误日志:日志默认位于 /usr/local/apisix/logs/error.log。
  • 语法错误:上传前使用 luac -p custom-header.lua 检查 Lua 语法。
  • 插件可见性:如果控制台插件列表中未出现该插件,请确认它已部署到正确的网关组。
  • Admin API 载荷结构:API7 网关创建路由时使用 paths 和 service_id,而不是 APISIX 风格的顶层 uri 和内联 upstream。
  • 名称冲突:确保插件名称唯一且不与内置插件冲突。

后续步骤​