本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文主要介绍kong 网关插件的编写,所用到的库,以及插件文件建结构等,测试用例采用开发一个简单的自定义权限认证插件。参考官网地址为
1. 文件结构介绍
- 一个简单的插件的文件结构如下:
simple-plugin
├── handler.lua
└── schema.lua
复制代码
- 一个功能比较复杂的插件文件结构如下:
complete-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│ ├── init.lua
│ └── 000_base_complete_plugin.lua
└── schema.lua
复制代码
- 字段解释
名字 | 是否必填 | 描述 |
---|---|---|
api.lua |
否 | 定义一个可以再UI管理界面调用的list,用以与插件实体本省进行交互。 |
daos.lua |
否 | 数据交互的相关文件,定义一些数据库交互信息,在插件使用数据库时使用。 |
handler.lua |
是 | 插件的核心逻辑,是一个接口,实现一些再kong的request和connection中各种操作。 |
migrations*/*.lua |
否 | 定义一些数据库脚本操作, 如创建表等,与daos.lua搭配使用。 |
schema.lua |
是 | 配置插件所需字段配置信息,以及一些字段校验操作。 |
2. 用户自定义逻辑接口介绍
用户自定义逻辑,主要是handler.lua
和schema.lua
这两个文件的一些逻辑,在开发过程中可以使用官方的模板github.com/Kong/kong-p… 进行开发,拉下代码后项目结构如下
在handler.lua文件的结构如下:
local CustomHandler = {
VERSION = "1.0.0",
PRIORITY = 10,
}
function CustomHandler:init_worker()
-- Implement logic for the init_worker phase here (http/stream)
kong.log("init_worker")
end
function CustomHandler:preread(config)
-- Implement logic for the preread phase here (stream)
kong.log("preread")
end
function CustomHandler:certificate(config)
-- Implement logic for the certificate phase here (http/stream)
kong.log("certificate")
end
function CustomHandler:rewrite(config)
-- Implement logic for the rewrite phase here (http)
kong.log("rewrite")
end
function CustomHandler:access(config)
-- Implement logic for the rewrite phase here (http)
kong.log("access")
end
function CustomHandler:header_filter(config)
-- Implement logic for the header_filter phase here (http)
kong.log("header_filter")
end
function CustomHandler:body_filter(config)
-- Implement logic for the body_filter phase here (http)
kong.log("body_filter")
end
function CustomHandler:log(config)
-- Implement logic for the log phase here (http/stream)
kong.log("log")
end
-- return the created table, so that Kong can execute it
return CustomHandler
复制代码
每一个方法会在不同的阶段执行,我们根据需求,实现相应方法就可以,下面着重介绍这几个方法;
方法名 | 链接 | 描述 |
---|---|---|
init_worker |
init_worker | 只在每个Nginx工作流程开始时执行. |
certificate |
ssl_certificate | 在SSL握手的SSL证书服务阶段执行。. |
rewrite |
rewrite | 作为重写阶段处理程序,在从客户端接收到每个请求时对其执行。在此阶段中,既没有标识“服务”也没有标识“消费者”,因此,只有将插件配置为全局插件时,才会执行此处理程序. |
access |
access | 针对来自客户端的每个请求在其被代理之前执行 |
response |
access | 已经被“header_filter()”和“body_filter()”替代。在从上游服务接收到整个响应之后,在发送给客户端之前执行. |
header_filter |
header_filter | 从上游服务接收到所有响应头字节时执行. |
body_filter |
body_filter | 针对从上游服务接收的响应主体的每个区块执行。由于响应流式传输回客户端,因此它可能超过缓冲区大小,并逐块流式传输。如果响应较大,可以多次调用此函数.详情可以查看 lua-nginx-module . |
log |
log | 将最后一个响应字节发送到客户端时执行. |
3. 开发自定义插件
- 插件思路
由konga管理界面配置请求的授权服务器,对path实现授权,其配置信息主要包括授权服务器endpoint, 请求方式,请求key 等信息
- 插件实现代码
- handler.lua
local httpUtil = require "resty.http"
local table_clear = require "table.clear"
local url = require "socket.url"
local cjson = require "cjson"
local encode_base64 = ngx.encode_base64
local pairs = pairs
local tonumber = tonumber
local fmt = string.format
local tostring = tostring
local queues = {} -- one queue per unique plugin config
local parsed_urls_cache = {}
local headers_cache = {}
local params_cache = {
ssl_verify = false,
headers = headers_cache,
}
local SelfPermissionHandler = {
PRIORITY = 1000, -- set the plugin priority, which determines plugin execution order
VERSION = "0.1", -- version in X.Y.Z format. Check hybrid-mode compatibility requirements.
}
local function check_customerKey(customerKey)
local ok
local err
return ok, err;
end
-- Parse host url.
-- @param `url` host url
-- @return `parsed_url` a table with host details:
-- scheme, host, port, path, query, userinfo
local function parse_url(host_url)
local parsed_url = parsed_urls_cache[host_url]
if parsed_url then
return parsed_url
end
parsed_url = url.parse(host_url)
if not parsed_url.port then
if parsed_url.scheme == "http" then
parsed_url.port = 80
elseif parsed_url.scheme == "https" then
parsed_url.port = 443
end
end
if not parsed_url.path then
parsed_url.path = "/"
end
parsed_urls_cache[host_url] = parsed_url
return parsed_url
end
-- Sends the provided payload (a string) to the configured plugin host
-- @return true if everything was sent correctly, falsy if error
-- @return error message if there was an error
local function send_payload(self, conf, payload)
local method = conf.method
local timeout = conf.timeout
local keepalive = conf.keepalive
local content_type = conf.content_type
local http_endpoint = conf.http_endpoint
local parsed_url = parse_url(http_endpoint)
local host = parsed_url.host
local port = tonumber(parsed_url.port)
local httpc = httpUtil.new()
httpc:set_timeout(timeout)
table_clear(headers_cache)
if conf.headers then
for h, v in pairs(conf.headers) do
headers_cache[h] = v
end
end
headers_cache["Host"] = parsed_url.host
headers_cache["Content-Type"] = content_type
headers_cache["Content-Length"] = #payload
if parsed_url.userinfo then
headers_cache["Authorization"] = "Basic " .. encode_base64(parsed_url.userinfo)
end
params_cache.method = method
params_cache.body = payload
params_cache.keepalive_timeout = keepalive
local url = fmt("%s://%s:%d%s", parsed_url.scheme, parsed_url.host, parsed_url.port, parsed_url.path)
-- note: `httpc:request` makes a deep copy of `params_cache`, so it will be
-- fine to reuse the table here
local res, err = httpc:request_uri(url, params_cache)
if not res then
return nil, "failed request to " .. host .. ":" .. tostring(port) .. ": " .. err
end
-- always read response body, even if we discard it without using it on success
local response_body = res.body
local success = res.status < 400
local err_msg
if not success then
err_msg = "request to " .. host .. ":" .. tostring(port) ..
" returned status code " .. tostring(res.status) .. " and body " ..
response_body
end
return success, err_msg
end
function SelfPermissionHandler:init_worker()
-- your custom code here
kong.log.debug("==================saying hi from the 'init_worker' handler")
end --]]
function SelfPermissionHandler:access(plugin_conf)
-- 1. 获取用户的请求头的key
local customerKey = kong.request.get_header("x-custom-key")
if not customerKey then
kong.log.err("====customer key is null, please check request")
return kong.response.error(402, "Please config a customer key for this request")
end
kong.log("====customer key is :", customerKey)
--2. 校验key 是否符合规则
local ok, err = check_customerKey(customerKey)
if err then
kong.log.err("====customer key is invalid, please check:", customerKey)
end
--3. 请求权限中心,获取该key的权限
local path = kong.request.get_path()
kong.log("====path is:", path)
local config_header_key = plugin_conf.header_key
kong.log("====config_header_key is:", config_header_key)
if config_header_key ~= customerKey then
return kong.response.error(403, "The key is invalid, please check")
end
local body ={
customerKey,
path,
}
body.customerKey=customerKey
body.path=path
local bodyStr = cjson.encode(body)
kong.log("====body is :", bodyStr)
local success, err_msg = send_payload(self, plugin_conf, bodyStr)
if err_msg then
kong.log.err("====Permission server error:", err_msg)
return kong.response.error(440, "Sorry, you don't have this resource permission, please check")
end
kong.log.inspect(plugin_conf)
end
-- return our plugin object
return SelfPermissionHandler
复制代码
- schema.lua
local typedefs = require "kong.db.schema.typedefs"
local url = require "socket.url"
return {
name = "self-permission",
fields = {
{ protocols = typedefs.protocols },
{ config = {
type = "record",
fields = {
-- NOTE: any field added here must be also included in the handler's get_queue_id method
{ http_endpoint = typedefs.url({ required = true, encrypted = true }) }, -- encrypted = true is a Kong-Enterprise exclusive feature, does nothing in Kong CE
{ method = { type = "string", default = "POST", one_of = { "POST", "PUT", "PATCH" }, }, },
{ content_type = { type = "string", default = "application/json", one_of = { "application/json" }, }, },
{ header_key = { type = "string", default = "", required=true },},
{ timeout = { type = "number", default = 10000 }, },
{ keepalive = { type = "number", default = 60000 }, },
{ retry_count = { type = "integer", default = 10 }, },
{ queue_size = { type = "integer", default = 1 }, },
{ flush_timeout = { type = "number", default = 2 }, },
{ headers = typedefs.headers {
keys = typedefs.header_name {
match_none = {
{
pattern = "^[Hh][Oo][Ss][Tt]$",
err = "cannot contain 'Host' header",
},
{
pattern = "^[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Ll][Ee][nn][Gg][Tt][Hh]$",
err = "cannot contain 'Content-Length' header",
},
{
pattern = "^[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]$",
err = "cannot contain 'Content-Type' header",
},
},
},
} },
},
custom_validator = function(config)
-- check no double userinfo + authorization header
local parsed_url = url.parse(config.http_endpoint)
if parsed_url.userinfo and config.headers then
for hname, hvalue in pairs(config.headers) do
if hname:lower() == "authorization" then
return false, "specifying both an 'Authorization' header and user info in 'http_endpoint' is not allowed"
end
end
end
return true
end,
},
},
},
}
复制代码
总结:
插件参考:http-log, rate-limting两个原生插件的源码,编写的,使用的库如下:
- local httpUtil = require "resty.http"
- local table_clear = require "table.clear"
- local url = require "socket.url"
- local cjson = require "cjson"