简介
前面洋洋洒洒写了那么多文章,Kong搭建、Konga搭建、Kong插件开发工具包、Lua算法实现等等,就为了这篇Kong插件开发铺垫,在进一步讨论之前,有必要再简要阐述下 Kong 是如何构建的,特别是它如何与 Nginx 集成,以及它与 Lua 脚本之间的关系。
使用 lua-nginx-module 模块可以在 Nginx 中启用 Lua 脚本功能,Kong 与 OpenResty 一起发布,OpenResty 中已经包含了 lua-nginx-module 模块,OpenResty 不是 Nginx 的分支,而是一组扩展 Nginx 功能的模块。
因此,Kong 是一个 Lua 应用程序,旨在加载和执行 Lua 模块(我们通常称之为"插件"),并且 Kong 还为此提供了整套开发环境,包括 SDK、数据库抽象、数据迁移等等。
插件由 Lua 模块组成,用户可以使用插件开发包(又称PDK),通过调用请求响应或者流交互实现各种功能,PDK 是一组 Lua 方法,插件可以使用它来促进 Kong 核心模块(或其它组件)与插件本身交互。
有关 PDK 的详情,请详见我的另一篇文章
文件结构
简介
插件其实是一组 Lua 模块,本章中描述的每个文件都可以视为一个单独的模块,如果它们的命名遵循某个约定,Kong 就会检测并加载该插件模块
kong.plugins.<plugin_name>.<module_name>
用户定义的插件模块需要通过 package.path
变量访问到,用户可以更改 lua_package_path
配置调整这个值,然而,安装插件的首选方法是通过 LuaRocks,它与 Kong 天然集成,有关 LuaRocks 安装插件的详情,请参考后面的章节。
为了让 Kong 意识到哪些插件需要安装,用户必须将它们添加到配置文件中 plugins 属性中,格式是以逗号分隔的列表,例如:
plugins = bundled,my-custom-plugin # your plugin name here
或者,用户不想加载任何预捆绑的插件:
plugins = my-custom-plugin # your plugin name here
现在,Kong 会试图从以下列命名空间中加载 Lua 模块
kong.plugins.my-custom-plugin.<module_name>
其中一些模块是必需的(例如:handler.lua
),有些是可选的,以允许插件实现一些额外的功能(例如:api.lua
可以扩展 Admin API 端点)
基础插件模块
最基础的插件,必需包含两个模块
lua-plugin
├── handler.lua
└── schema.lua
- handler.lua:插件的核心,它是需要实现的接口,其中每个方法会在请求/连接的生命周期中运行
- schema.lua:插件可能需要保留一些用户输入的配置,此模板定义一些规则保存配置的模式,以便用户只能输入有效的配置项
高级插件模块
有些插件与 Kong 之间有更深入的集成,比如在数据库中存数据,在 Admin API 中公开端点等等,每个插件都可以通过向插件添加新模块来完成,插件的结构大致如下
lua-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│ ├── init.lua
│ └── base_complete_plugin.lua
└── schema.lua
简要说明如下:
模块名 | 是否必须 | 描述 |
---|---|---|
api.lua | 否 | 定义 Admin API 中也用的端点列表,与插件自定义的实体进行交互 |
daos.lua | 否 | 定义数据库访问对象列表 |
handler.lua | 是 | 一个需要实现的接口,其中每个方法会在请求/连接的生命周期中运行 |
migrations/*.lua | 否 | 数据源迁移,只有当用户的插件有自定义实体时才需要 |
schema.lua | 是 | 保存插件的配置项,一边用户只能输入有效的配置值 |
Key-Auth 插件实现了整套完整的插件接口,可以查看源码了解细节
实现自定义逻辑
简介
Kong 的插件允许用户在整个生命周期的几个切点加入自定义逻辑,为此,必须实现 base_plugin.lua
接口中的一些方法,这些方法在 kong.plugins.<plugin_name>.handler
模块中实现。
模块
kong.plugins.<plugin_name>.handler
可用的上下文
插件接口允许用户覆盖 handler.lua
文件中的以下任何方法,在 Kong 的执行生命周期的各个切点实现自定义逻辑:
HTTP Module:为 HTTP / HTTPS 请求编写的插件
方法名 | 段信息 | 描述 |
---|---|---|
:init_worker() | init_worker | 每个 Nginx worker 进程启动时执行 |
:certificate() | ssl_certificate | 在 SSL 握手提供证书时执行 |
:rewrite() | rewrite | 从客户端接收到请求,进入 rewrite 段执行,注意,在这个阶段没有识别服务,也没有消费者介入,只有配置成全局插件才会执行此处理程序 |
:access() | access | 从客户端接收到请求到被代理到 upstream service 之前执行 |
:header_filter() | header_filter | 从 upstream service 接收到所有响应头时执行 |
:body_filter() | body_filter | 针对从 upstream service 接收到的响应体块执行,由于响应以流的形式返回给客户端,超过缓冲区大小的按块进行传输,因此,如果响应体很大,会多次调用这个方法 |
:log() | log | 最后一个响应字节发送到客户端时执行 |
Stream Module:为 TCP 流连接编写的插件
方法名 | 段信息 | 描述 |
---|---|---|
:init_worker() | init_worker | 每个 Nginx worker 进程启动时执行 |
:preread() | preread | 每个连接执行一次 |
:log() | log | 每个连接中断执行一次 |
除了 :init_worker()
方法,每个方法都会携带一个参数,这个参数由 Kong 给出,即插件的配置,这个参数的类型是 Lua table,包含了用户定义的值,格式根据用户定义的插件 schema 格式
handler.lua 格式
handler.lua
文件需要返回一个 table,里面包含了用户希望执行的方法,为了方便起见,这里给大家看一下我自定义的 sign-aes256 加解密插件的示例,代码如下:
local kong = kong
local pcall = pcall
local get_header = kong.request.get_header
local set_header = kong.service.request.set_header
local set_raw_body = kong.service.request.set_raw_body
local ngx_decode_args = ngx.decode_args
local encode_args = ngx.encode_args
local str_find = string.find
local multipart = require "multipart"
local cjson = require "cjson"
local aes = require("resty.aes_ecb")
local CONTENT_TYPE = "content-type"
local CONTENT_LENGTH = "content-length"
local JSON, MULTI, ENCODED = "json", "multi_part", "form_encoded"
local RequestSignAes256Handler = {}
local function decode_args(body)
if body then
return ngx_decode_args(body)
end
return {}
end
local function parse_json(body)
if body then
local status, res = pcall(cjson.decode, body)
if status then
return res
end
end
end
local function get_content_type(content_type)
if content_type == nil then
return
end
if str_find(content_type:lower(), "application/json", nil, true) then
return JSON
elseif str_find(content_type:lower(), "multipart/form-data", nil, true) then
return MULTI
elseif str_find(content_type:lower(), "application/x-www-form-urlencoded", nil, true) then
return ENCODED
end
end
-- 请求时的处理过程
function RequestSignAes256Handler:access(conf)
local content_type_value = get_header(CONTENT_TYPE)
local content_type = get_content_type(content_type_value)
local params, contenttype
local body = kong.request.get_raw_body()
if content_type == ENCODED then
parameters = decode_args(body)
params = parameters["params"]
elseif content_type == MULTI then
parameters = multipart(body and body or "", content_type_value)
params = parameters:get("params").value
elseif content_type == JSON then
parameters = parse_json(body)
params = parameters["params"]
end
if params == nil then
return kong.response.exit(200, { code = 20000, data = "", msg = "Missing required parameters" })
end
local ecb = aes:new()
local aes_baseparams = ecb:decrypt(conf.key,dec(params) )
if content_type == ENCODED then
parameters["params"] = aes_baseparams
body = encode_args(parameters)
elseif content_type == MULTI then
parameters:delete("params")
parameters:set_simple("params", aes_baseparams)
body = parameters:tostring()
elseif content_type == JSON then
parameters["params"] = aes_baseparams
body = cjson.encode(parameters)
end
set_raw_body(body)
set_header(CONTENT_LENGTH, #body)
end
-- PRIORITY 越大执行顺序越靠前
RequestSignAes256Handler.PRIORITY = 800
RequestSignAes256Handler.VERSION = "1.0.0"
return RequestSignAes256Handler
插件配置
简介
大多数情况下,插件的配置可以满足用户的需求,插件的配置存储在数据库中,当插件运行时,Kong 在数据库中检索出它们,并将其传递给 handler.lua 方法
配置在 Kong 中由 Lua table 组成,我们称之为 schema,用户通过 Admin API 启用插件时,以键值对的形式输入参数,Kong 提供了验证用户插件配置的方法,当用户向 Admin API 发送请求启用或更新给定 Service、Route 或 Consumer 上的插件时,Kong 会根据用户定义的 schema 来验证插件配置,举例,用户执行如下请求:
curl -X POST http://kong:8001/services/<service-name-or-id>/plugins -d "name=my-custom-plugin" -d "config.foo=bar"
如果配置对象的所有属性都验证有效,API 会返回 201 Created,插件将和配置一起存储在数据库中:
{
foo = "bar"
}
如果配置验证不通过,API 会返回 400 Bad Request 和错误信息
模块
kong.plugins.<plugin_name>.schema
schema.lua 格式
这个模块返回一个 Lua table,其中包含了用户可以配置插件哪些属性,可用的属性包含:
属性名 | 数据类型 | 描述 |
---|---|---|
name | string | 插件名称,比如 key-auth |
fields | table | 字段定义数组 |
entity_checks | function | 校验条件数组 |
所有插件都默认继承的属性:
属性名 | 数据类型 | 描述 |
---|---|---|
id | string | 自动生成的插件 Id |
name | string | 插件名称,比如 key-auth |
created_at | number | 插件配置时间 |
route | table | 绑定的路由 |
service | table | 绑定的服务 |
consumer | table | 绑定的消费者 |
run_on | string | 插件运行在服务网格上的哪个节点 |
protocols | table | 插件运行的协议 |
table | boolean | 插件是否生效 |
tags | table | 插件的标签 |
大多数情况下,用户可以使用默认值,或者让用户在启用插件时指定值,以下是一份我自定义插件中写的简单 schema.lua
文件:
local typedefs = require "kong.db.schema.typedefs"
return {
name = "request-sign-aes256",
fields = {
{ consumer = typedefs.no_consumer },
{ protocols = typedefs.protocols_http },
{ config = {
type = "record",
fields = {
{ key = { type = "string", default = "12345678912345678912345678912345" }, },
},
},
},
},
}
这里罗列了一些常用的属性规则:
规则 | 描述 |
---|---|
type | 属性的类型 |
required | 属性是否是必须的 |
default | 属性的默认值 |
elements | array 或 set 格式的元素类型 |
keys | map 格式的 key 元素类型 |
values | map 格式的 value 元素类型 |
fields | record 格式的元素类型 |
between | 校验输入是否在约定的范围之内 |
eq | 校验输入是否等于约定值 |
ne | 校验输入是否不等于约定值 |
gt | 校验输入是否大于约定值 |
len_eq | 校验输入字符串长度是否等于约定值 |
len_min | 校验输入字符串长度是否大于约定值 |
len_max | 校验输入字符串长度是否小于约定值 |
match | 校验输入字符串是否匹配约定正则表达式 |
not_match | 校验输入字符串是否不匹配约定正则表达式 |
match_all | 校验输入字符串是否全部匹配约定正则表达式列表 |
match_none | 校验输入字符串是否全部不匹配约定正则表达式列表 |
match_any | 校验输入字符串是否匹配约定正则表达式列表中的一个 |
starts_with | 校验输入字符串是否以约定值开头 |
one_of | 校验输入字符串是否是约定值列表中的一个 |
contains | 校验输入字符串列表是否包含约定值 |
is_regex | 校验输入字符串是否是合法的正则表达式 |
custom_validator | 校验输入是否是标准的 Lua 方法 |
我的自定义插件 schema.lua
文件比较简单,想要了解上面的一些属性规则具体使用,可以参考 Kong 的自带插件 key-auth 等。
启动我的自定义插件,在插件,Other tab下,有很多我的自定义插件,如下:
选择在本文示例的 Request Sign Aes256
插件,添加
大功告成,所有的请求内容都需要进行 aes256 加密才可,由我的插件解密成明文,再发给原来的服务。