本文主要是总结在Apisix插件开发过程中积累的一些经验,插件开发之后如何部署生效网上有很多资料,这里就不再说明。
Apisix的插件逻辑写在插件名称.lua文件中,文件结构如下:
如:example-plugin.lua
local core = require("apisix.core")
-- 插件的名字
local plugin_name = "example-plugin"
-- 插件的基本信息
local _M = {
version = 0.1,
priority = 0, -- 优先级
name = plugin_name,
schema = schema,
metadata_schema = metadata_schema,
}
-- 配置描述
local schema = {
type = "object",
properties = {
i = {type = "number", minimum = 0},
s = {type = "string"},
t = {type = "array", minItems = 1},
ip = {type = "string"},
port = {type = "integer"},
},
required = {"i"},
}
-- 元数据配置
local metadata_schema = {
type = "object",
properties = {
ikey = {type = "number", minimum = 0},
skey = {type = "string"},
},
required = {"ikey", "skey"},
}
-- 校验配置合法性
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-- 阶段方法
function _M.access(conf, ctx)
core.log.warn(core.json.encode(conf))
core.log.warn(core.json.encode(ctx, true))
......
end
local function hello()
local args = ngx.req.get_uri_args()
if args["json"] then
return 200, {msg = "world"}
else
return 200, "world\n"
end
end
-- 暴露api的接口
function _M.control_api()
return {
{
methods = {"GET"},
uris = {"/v1/plugin/example-plugin/hello"},
handler = hello,
}
}
end
下面结合example-plugin.lua文件对插件的开发逻辑进行说明:
1. 优先级
新插件的优先级(priority 属性)不能与现有插件的优先级相同,您可以使用 control API 的 /v1/schema 方法查看所有插件的优先级。另外,同一个阶段里面,优先级 ( priority ) 值大的插件,会优先执行,比如 example-plugin 的优先级是 0,ip-restriction 的优先级是 3000,所以在每个阶段,会先执行 ip-restriction 插件,再去执行 example-plugin 插件。
对于自定义的插件,优先建议采用 1 到 99 之间的优先级,但是也可以根据实际情况定义更大的优先级,不和现有插件的优先级相同就可以。
2. _M 中还有其他字段会影响到插件的行为。
local _M = {
...
type = 'auth',
run_policy = 'prefer_route',
}
run_policy 字段可以用来控制插件执行。当这个字段设置成 prefer_route 时,且该插件同时配置在全局和路由级别,那么只有路由级别的配置生效。
当一个插件设置 type = 'auth',说明它是个认证插件。认证插件需要在执行后选择对应的 consumer。
举个例子,在 key-auth 插件中,它通过 apikey 请求头获取对应的 consumer,然后通过 consumer.attach_consumer 设置它。
为了跟 consumer 资源一起使用,认证插件需要提供一个 consumer_schema 来检验 consumer 资源的 plugins 属性里面的配置。
下面是 key-auth 插件的 consumer 配置:
{
"username": "Joe",
"plugins": {
"key-auth": {
"key": "Joe's key"
}
}
}
你在创建 Consumer 时会用到它。
为了检验这个配置,这个插件使用了如下的 schema:
local consumer_schema = {
type = "object",
properties = {
key = {type = "string"},
},
required = {"key"},
}
注意 key-auth 的 check_schema(conf) 方法和 example-plugin 的同名方法的区别:
-- key-auth
function _M.check_schema(conf, schema_type)
if schema_type == core.schema.TYPE_CONSUMER then
return core.schema.check(consumer_schema, conf)
else
return core.schema.check(schema, conf)
end
end
-- example-plugin
function _M.check_schema(conf, schema_type)
return core.schema.check(schema, conf)
end
3. 配置描述与校验
schema定义插件的配置项(即启用插件时需要在插件上配置的变量,通过conf传给插件源码),以及对应的 JSON Schema 描述,并使用check_schema方法完成对 JSON 的校验,这样方便对配置的数据规格进行验证,以确保数据的完整性以及程序的健壮性。
如果插件需要使用一些元数据,可以定义插件的 metadata_schema ,然后就可以通过 Admin API 动态的管理这些元数据了。
上面的示例定义的是字符串和数字类型的对象形式,如果要定义数组类型的对象,可以使用如下方式:
local schema = {
type = "object",
properties = {
-- version_flag为字符串类型
version_flag = {
type = "string",
default = "version",
},
-- network为字符串类型
network = {
type = "string",
default = "test",
},
-- network_routerlist变量为数组类型
network_routerlist = {
type = "array",
items = {
type = "object",
properties = {
network_id = {type = "string"},
end_point = {type="string"},
}
},
}
},
}
启用插件后,在插件的配置中用以下格式设置变量:
4. 执行阶段
根据业务功能,确定你的插件需要在哪个阶段执行。key-auth 是一个认证插件,所以需要在 rewrite 阶段执行。在 APISIX,只有认证逻辑可以在 rewrite 阶段里面完成,其他需要在代理到上游之前执行的逻辑都是在 access 阶段完成的。常见的阶段包括:init, rewrite, access, balancer, header filter, body filter 以及 log。
个人理解,插件在哪个阶段执行,就在插件lua文件中在对应阶段方法中编写插件的功能逻辑。如function _M.access(conf, ctx) ... end,function _M.log(conf, ctx)...end
在阶段方法中具有 conf 和 ctx 两个参数,
conf 参数是插件的相关配置信息,即启用插件时需要传递的参数就存在conf中,您可以通过 core.log.warn(core.json.encode(conf)) 将其输出到 error.log 中进行查看。
ctx 参数缓存了请求相关的数据信息,您可以通过 core.log.warn(core.json.encode(ctx, true)) 将其输出到 error.log 中进行查看,示例如下:
function _M.access(conf, ctx)
core.log.warn(core.json.encode(conf))
core.log.warn(core.json.encode(ctx, true))
end
如果插件需要修改路由的配置或者调整路由匹配的上游,则可以使用api_ctx获取路由对象,然后对路由对象进行修改即可。
如下示例:
function _M.access(conf, ctx)
local api_ctx = ngx.ctx.api_ctx
local route = api_ctx.matched_route --获取当前的路由对象
local upstreamId = "newupstream"
local originuri = "/newpath"
core.log.warn("before set route============route info is: "..core.json.encode(route))
route.value.plugins["proxy-rewrite"].uri = originuri
route.value.upstream_id = upstreamId
core.log.warn("after set route=============route info is: "..core.json.encode(ctx.var._ctx, true))
core.log.warn("value of route is:============="..core.json.encode(route))
end
注意: local api_ctx = ngx.ctx.api_ctx需要在函数里面定义才会获取到ngx.ctx.api_ctx的值,如果把local api_ctx的声明放在函数外面作为整个插件的局部变量,那获取到的ngx.ctx.api_ctx的值为nil。
5. 注册公共接口
插件可以使用api()注册暴露给公网的接口。以 jwt-auth 插件为例,这个插件为了让客户端能够签名,注册了 GET /apisix/plugin/jwt/sign 这个接口:
local function gen_token()
-- ...
end
function _M.api()
return {
{
methods = {"GET"},
uri = "/apisix/plugin/jwt/sign",
handler = gen_token,
}
}
end
注意,注册的接口将不会默认暴露,需要结合使用public-api 插件来暴露它。
6. 注册控制接口
如果你只想暴露 API 到 localhost 或内网,你可以通过 Control API 来暴露它。
示例:
local function hello()
local args = ngx.req.get_uri_args()
if args["json"] then
return 200, {msg = "world"}
else
return 200, "world\n"
end
end
function _M.control_api()
return {
{
methods = {"GET"},
uris = {"/v1/plugin/example-plugin/hello"},
handler = hello,
}
}
end
如果你没有改过默认的 control API 配置,这个插件暴露的 GET /v1/plugin/example-plugin/hello API 只有通过 127.0.0.1 才能访问它。通过以下命令进行测试:
curl -i -X GET "http://127.0.0.1:9090/v1/plugin/example-plugin/hello"
其中,9090是control api的端口,可以在apisix的config.yaml或者config-default.yaml中看到control的port是多少。
7. 注册自定义变量
我们可以在 APISIX 的许多地方使用变量。例如,在 http-logger 中自定义日志格式,用它作为 limit-* 插件的键。在某些情况下,内置的变量是不够的。因此,APISIX 允许开发者在全局范围内注册他们的变量,并将它们作为普通的内置变量使用。
例如,让我们注册一个叫做 a6_labels_zone 的变量来获取路由中 zone 标签的值。
local core = require "apisix.core"
core.ctx.register_var("a6_labels_zone", function(ctx)
local route = ctx.matched_route and ctx.matched_route.value
if route and route.labels then
return route.labels.zone
end
return nil
end)
此后,任何对 $a6_labels_zone 的获取操作都会调用注册的获取器来获取数值。
注意,自定义变量不能用于依赖 Nginx 指令的功能,如 access_log_format。