Kong 插件开发指南

Plugin Development

Posted by ms2008 on June 19, 2018

Kong 的插件使用了一个叫 Classic 的 class 机制。所有的插件都是从 base_plugin.lua 基类上继承而来。base_plugin.lua 定义了插件在各个阶段被执行的方法名:

function BasePlugin:init_worker()
  ngx_log(DEBUG, "executing plugin \"", self._name, "\": init_worker")
end

function BasePlugin:certificate()
  ngx_log(DEBUG, "executing plugin \"", self._name, "\": certificate")
end

function BasePlugin:rewrite()
  ngx_log(DEBUG, "executing plugin \"", self._name, "\": rewrite")
end

function BasePlugin:access()
  ngx_log(DEBUG, "executing plugin \"", self._name, "\": access")
end

function BasePlugin:header_filter()
  ngx_log(DEBUG, "executing plugin \"", self._name, "\": header_filter")
end

function BasePlugin:body_filter()
  ngx_log(DEBUG, "executing plugin \"", self._name, "\": body_filter")
end

function BasePlugin:log()
  ngx_log(DEBUG, "executing plugin \"", self._name, "\": log")
end

根据方法名也可以看出,这 7 个方法对应于 OpenResty 的不同执行阶段。也就是说插件只能对外暴露出这 7 个方法名中的一个或多个才能被 Kong 的插件机制执行,接下来 Kong 会在 OpenResty 不同的执行阶段,执行插件对应的方法。

1. 插件的结构

一个完整的插件目录结构应该像下面这样:

complete-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│   ├── cassandra.lua
│   └── postgres.lua
└── schema.lua

其各个模块的功能如下:

Module name Required Description
api.lua No 插件需要向 Admin API 暴露接口时使用
daos.lua No 数据层相关,当插件需要访问数据库时配置
handler.lua Yes 插件的主要逻辑,这个将会被 Kong 在不同阶段执行其对应的 handler
migrations/*.lua No 插件依赖的数据表结构,启用了 daos.lua 时需要定义
schema.lua Yes 插件的配置参数定义,主要用于 Kong 参数验证

其中 handler.luaschema.lua 是必需的,上面提到的插件需要暴露出来的方法就定义在 handler.lua 中。

2. 逻辑实现

这里以 Kong 自带的 request-termination 插件为例,分析其实现原理。其 handler.lua 的头部是这样定义的:

-- 引入插件基类
local BasePlugin = require "kong.plugins.base_plugin"
local responses = require "kong.tools.responses"

-- 派生出一个子类,其实这里是为了继承来自 Classic 的 __call 元方法,
-- 方便 Kong 在 init 阶段预加载插件的时候执行构造函数 new()
local RequestTerminationHandler = BasePlugin:extend()

-- 设置插件的优先级,Kong 将按照插件的优先级来确定其执行顺序(越大越优先)
-- 需要注意的是应用于 Consumer 的插件因为依赖于 Auth,所以 Auth 类插件优先级普遍比较高
RequestTerminationHandler.PRIORITY = 2
RequestTerminationHandler.VERSION = "0.1.0"

接下来就是定义插件的公有方法:

-- 插件的构造函数,用于初始化插件的 _name 属性,后面会根据这个属性打印插件名
-- 其实这个方法不是必须的,只是用于插件调试
function RequestTerminationHandler:new()
  RequestTerminationHandler.super.new(self, "request-termination")
end

-- 表明需要在 access 阶段执行此插件
function RequestTerminationHandler:access(conf)
  -- 执行父类的 access 方法,其实就是为了调试时输出日志用的
  RequestTerminationHandler.super.access(self)

  -- 接下来的就是插件的主要逻辑,不再赘述
  local status_code = conf.status_code
  local content_type = conf.content_type
  local body = conf.body
  local message = conf.message
  if body then
    ngx.status = status_code

    if not content_type then
      content_type = "application/json; charset=utf-8";
    end
    ngx.header["Content-Type"] = content_type
    ngx.header["Server"] = server_header

    ngx.say(body)

    return ngx.exit(status_code)
  else
    return responses.send(status_code, message)
  end
end

return RequestTerminationHandler

3. 参数定义

插件的参数定义在 schema.lua 中,类似于 JSON Schema,主要用于描述插件参数的数据格式:

local Errors = require "kong.dao.errors"

return {
  -- 描述插件参数的数据格式,用于 Kong 验证参数
  fields = {
    status_code = { type = "number", default = 503 },
    message = { type = "string" },
    content_type = { type = "string" },
    body = { type = "string" },
  },
  -- 自定义更为细粒度的参数校验
  self_check = function(schema, plugin_t, dao, is_updating)
    if plugin_t.status_code then
      if plugin_t.status_code < 100 or plugin_t.status_code > 599 then
        return false, Errors.schema("status_code must be between 100 .. 599")
      end
    end

    if plugin_t.message then
      if plugin_t.content_type or plugin_t.body then
        return false, Errors.schema("message cannot be used with content_type or body")
      end
    else
      if plugin_t.content_type and not plugin_t.body then
        return false, Errors.schema("content_type requires a body")
      end
    end

    return true
  end
}

当传入的参数无法通过 Kong 的校验时,插件配置将会失败,比如:

# 传入未定义的参数:foo
curl -s -i -X POST \
  --url http://localhost:8001/plugins/ \
  --data 'name=request-termination' \
  --data 'config.foo=bar'

HTTP/1.1 400 Bad Request
Date: Wed, 13 Jun 2018 15:03:39 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.12.3

{"config.foo":"foo is an unknown field"}

# 传入的 status_code 不是合法的数据
curl -s -i -X POST \
  --url http://localhost:8001/plugins/ \
  --data 'name=request-termination' \
  --data 'config.status_code=88'

HTTP/1.1 400 Bad Request
Date: Wed, 13 Jun 2018 15:05:12 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.12.3

{"config":"status_code must be between 100 .. 599"}

上面只是一个简单的应用,其实 Kong 还可以实现更为复杂的参数校验,具体可以参考这里:Plugin Development - Store Configuration