1. 插件系统概述

1.1 什么是Kong插件

Kong插件是扩展Kong功能的模块,可以在请求/响应的生命周期中执行自定义逻辑。插件使用Lua语言开发,基于OpenResty平台运行。

1.2 插件的作用

  • 认证和授权: 验证用户身份和权限
  • 安全防护: 防止恶意攻击和滥用
  • 流量控制: 限制请求频率和大小
  • 监控和日志: 收集指标和记录日志
  • 数据转换: 修改请求和响应数据
  • 协议转换: 支持不同的通信协议

1.3 插件执行顺序

Kong按照优先级顺序执行插件,优先级数字越小,执行越早:

认证插件 (1000) -> 安全插件 (900) -> 流量控制 (800) -> 转换插件 (700)

2. 内置插件介绍

2.1 认证插件

2.1.1 Basic Authentication

# 启用Basic Auth插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=basic-auth"

# 创建消费者
curl -X POST http://localhost:8001/consumers \
  --data "username=testuser"

# 为消费者添加凭证
curl -X POST http://localhost:8001/consumers/testuser/basic-auth \
  --data "username=user" \
  --data "password=pass"

2.1.2 JWT Authentication

# 启用JWT插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=jwt"

# 为消费者创建JWT凭证
curl -X POST http://localhost:8001/consumers/testuser/jwt \
  --data "key=my-key" \
  --data "secret=my-secret"

2.1.3 OAuth 2.0

# 启用OAuth2插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=oauth2" \
  --data "config.scopes=read,write" \
  --data "config.mandatory_scope=true"

2.2 安全插件

2.2.1 Rate Limiting

# 启用限流插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=rate-limiting" \
  --data "config.minute=100" \
  --data "config.hour=1000"

2.2.2 IP Restriction

# IP白名单
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=ip-restriction" \
  --data "config.allow=192.168.1.0/24,10.0.0.0/8"

# IP黑名单
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=ip-restriction" \
  --data "config.deny=192.168.1.100"

2.2.3 CORS

# 启用CORS插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=cors" \
  --data "config.origins=*" \
  --data "config.methods=GET,POST,PUT,DELETE" \
  --data "config.headers=Accept,Content-Type,Authorization"

2.3 监控插件

2.3.1 Prometheus

# 启用Prometheus插件
curl -X POST http://localhost:8001/plugins \
  --data "name=prometheus"

# 访问指标
curl http://localhost:8001/metrics

2.3.2 StatsD

# 启用StatsD插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=statsd" \
  --data "config.host=statsd-server" \
  --data "config.port=8125"

2.4 转换插件

2.4.1 Request Transformer

# 请求转换插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=request-transformer" \
  --data "config.add.headers=X-Custom-Header:value" \
  --data "config.add.querystring=new_param:value"

2.4.2 Response Transformer

# 响应转换插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=response-transformer" \
  --data "config.add.headers=X-Response-Header:value" \
  --data "config.remove.headers=Server"

3. 自定义插件开发

3.1 插件结构

3.1.1 目录结构

my-plugin/
├── kong/
│   └── plugins/
│       └── my-plugin/
│           ├── handler.lua
│           ├── schema.lua
│           └── migrations/
│               └── 000_base_my_plugin.lua
└── my-plugin-1.0.0-1.rockspec

3.1.2 Rockspec文件

-- my-plugin-1.0.0-1.rockspec
package = "my-plugin"
version = "1.0.0-1"
source = {
   url = "git://github.com/username/my-plugin",
   tag = "v1.0.0"
}
description = {
   summary = "My custom Kong plugin",
   detailed = "A detailed description of my plugin",
   homepage = "https://github.com/username/my-plugin",
   license = "MIT"
}
dependencies = {
   "lua >= 5.1"
}
build = {
   type = "builtin",
   modules = {
      ["kong.plugins.my-plugin.handler"] = "kong/plugins/my-plugin/handler.lua",
      ["kong.plugins.my-plugin.schema"] = "kong/plugins/my-plugin/schema.lua",
   }
}

3.2 Schema定义

3.2.1 基本Schema

-- kong/plugins/my-plugin/schema.lua
local typedefs = require "kong.db.schema.typedefs"

return {
  name = "my-plugin",
  fields = {
    { consumer = typedefs.no_consumer },
    { protocols = typedefs.protocols_http },
    { config = {
        type = "record",
        fields = {
          { message = {
              type = "string",
              default = "Hello World",
              required = true,
          }},
          { timeout = {
              type = "number",
              default = 1000,
              between = { 1, 60000 },
          }},
          { enabled = {
              type = "boolean",
              default = true,
          }},
        },
    }},
  },
}

3.2.2 复杂Schema示例

local typedefs = require "kong.db.schema.typedefs"

return {
  name = "advanced-plugin",
  fields = {
    { consumer = typedefs.no_consumer },
    { protocols = typedefs.protocols_http },
    { config = {
        type = "record",
        fields = {
          { api_key = {
              type = "string",
              required = true,
              encrypted = true,  -- 加密存储
          }},
          { allowed_ips = {
              type = "array",
              elements = typedefs.ip,
              default = {},
          }},
          { rate_limit = {
              type = "record",
              fields = {
                { requests = { type = "number", default = 100 }},
                { window = { type = "number", default = 60 }},
              },
          }},
          { headers = {
              type = "map",
              keys = { type = "string" },
              values = { type = "string" },
          }},
        },
    }},
  },
}

3.3 Handler实现

3.3.1 基本Handler

-- kong/plugins/my-plugin/handler.lua
local BasePlugin = require "kong.plugins.base_plugin"
local MyPluginHandler = BasePlugin:extend()

MyPluginHandler.PRIORITY = 1000
MyPluginHandler.VERSION = "1.0.0"

function MyPluginHandler:new()
  MyPluginHandler.super.new(self, "my-plugin")
end

function MyPluginHandler:access(conf)
  MyPluginHandler.super.access(self)
  
  -- 在这里实现插件逻辑
  kong.response.set_header("X-My-Plugin", conf.message)
end

return MyPluginHandler

3.3.2 完整生命周期Handler

local BasePlugin = require "kong.plugins.base_plugin"
local json = require "cjson"
local http = require "resty.http"

local AdvancedPluginHandler = BasePlugin:extend()

AdvancedPluginHandler.PRIORITY = 1000
AdvancedPluginHandler.VERSION = "1.0.0"

function AdvancedPluginHandler:new()
  AdvancedPluginHandler.super.new(self, "advanced-plugin")
end

-- 初始化阶段
function AdvancedPluginHandler:init_worker()
  AdvancedPluginHandler.super.init_worker(self)
  -- 初始化工作进程
end

-- 证书阶段
function AdvancedPluginHandler:certificate(conf)
  AdvancedPluginHandler.super.certificate(self)
  -- SSL证书处理
end

-- 重写阶段
function AdvancedPluginHandler:rewrite(conf)
  AdvancedPluginHandler.super.rewrite(self)
  -- URL重写逻辑
end

-- 访问阶段
function AdvancedPluginHandler:access(conf)
  AdvancedPluginHandler.super.access(self)
  
  -- IP检查
  local client_ip = kong.client.get_ip()
  if not self:is_ip_allowed(client_ip, conf.allowed_ips) then
    return kong.response.exit(403, { message = "IP not allowed" })
  end
  
  -- 速率限制
  if not self:check_rate_limit(conf.rate_limit) then
    return kong.response.exit(429, { message = "Rate limit exceeded" })
  end
  
  -- 添加自定义头
  for key, value in pairs(conf.headers or {}) do
    kong.service.request.set_header(key, value)
  end
end

-- 响应阶段
function AdvancedPluginHandler:header_filter(conf)
  AdvancedPluginHandler.super.header_filter(self)
  -- 修改响应头
end

-- 响应体阶段
function AdvancedPluginHandler:body_filter(conf)
  AdvancedPluginHandler.super.body_filter(self)
  -- 修改响应体
end

-- 日志阶段
function AdvancedPluginHandler:log(conf)
  AdvancedPluginHandler.super.log(self)
  
  -- 记录访问日志
  local log_data = {
    timestamp = ngx.time(),
    client_ip = kong.client.get_ip(),
    method = kong.request.get_method(),
    uri = kong.request.get_path(),
    status = kong.response.get_status(),
    latency = kong.ctx.shared.response_time
  }
  
  kong.log.info(json.encode(log_data))
end

-- 辅助方法
function AdvancedPluginHandler:is_ip_allowed(ip, allowed_ips)
  if #allowed_ips == 0 then
    return true
  end
  
  for _, allowed_ip in ipairs(allowed_ips) do
    if ip == allowed_ip then
      return true
    end
  end
  
  return false
end

function AdvancedPluginHandler:check_rate_limit(rate_config)
  -- 实现速率限制逻辑
  local key = "rate_limit:" .. kong.client.get_ip()
  local current_requests = kong.cache.get(key) or 0
  
  if current_requests >= rate_config.requests then
    return false
  end
  
  kong.cache.set(key, current_requests + 1, rate_config.window)
  return true
end

return AdvancedPluginHandler

3.4 数据库迁移

3.4.1 迁移文件

-- kong/plugins/my-plugin/migrations/000_base_my_plugin.lua
return {
  postgres = {
    up = [[
      CREATE TABLE IF NOT EXISTS "my_plugin_data" (
        "id"           UUID                         PRIMARY KEY,
        "created_at"   TIMESTAMP WITH TIME ZONE     DEFAULT (CURRENT_TIMESTAMP(0) AT TIME ZONE 'UTC'),
        "consumer_id"  UUID                         REFERENCES "consumers" ("id") ON DELETE CASCADE,
        "key"          TEXT                         UNIQUE,
        "value"        TEXT
      );
      
      CREATE INDEX IF NOT EXISTS "my_plugin_data_consumer_id_idx" ON "my_plugin_data" ("consumer_id");
    ]],
    teardown = function(connector)
      return connector:query([[
        DROP TABLE "my_plugin_data";
      ]])
    end,
  },
  
  cassandra = {
    up = [[
      CREATE TABLE IF NOT EXISTS my_plugin_data (
        id          uuid PRIMARY KEY,
        created_at  timestamp,
        consumer_id uuid,
        key         text,
        value       text
      );
      
      CREATE INDEX IF NOT EXISTS ON my_plugin_data (consumer_id);
    ]],
    teardown = function(connector)
      return connector:query("DROP TABLE my_plugin_data")
    end,
  },
}

4. 插件安装和管理

4.1 安装自定义插件

4.1.1 使用LuaRocks安装

# 从本地安装
luarocks make

# 从Git仓库安装
luarocks install my-plugin --from=https://github.com/username/my-plugin

# 从rockspec文件安装
luarocks install my-plugin-1.0.0-1.rockspec

4.1.2 手动安装

# 复制插件文件
sudo cp -r kong/plugins/my-plugin /usr/local/share/lua/5.1/kong/plugins/

# 更新Kong配置
echo "plugins = bundled,my-plugin" >> /etc/kong/kong.conf

# 重启Kong
kong restart

4.2 插件配置管理

4.2.1 全局插件

# 启用全局插件
curl -X POST http://localhost:8001/plugins \
  --data "name=my-plugin" \
  --data "config.message=Global Hello"

# 查看全局插件
curl http://localhost:8001/plugins

4.2.2 服务级插件

# 为特定服务启用插件
curl -X POST http://localhost:8001/services/{service}/plugins \
  --data "name=my-plugin" \
  --data "config.message=Service Hello"

4.2.3 路由级插件

# 为特定路由启用插件
curl -X POST http://localhost:8001/routes/{route}/plugins \
  --data "name=my-plugin" \
  --data "config.message=Route Hello"

4.2.4 消费者级插件

# 为特定消费者启用插件
curl -X POST http://localhost:8001/consumers/{consumer}/plugins \
  --data "name=my-plugin" \
  --data "config.message=Consumer Hello"

4.3 插件调试

4.3.1 日志调试

-- 在插件中添加日志
kong.log.debug("Debug message")
kong.log.info("Info message")
kong.log.warn("Warning message")
kong.log.err("Error message")

4.3.2 使用ngx.log

-- 直接使用nginx日志
ngx.log(ngx.ERR, "Error occurred: ", error_message)
ngx.log(ngx.INFO, "Request processed: ", request_id)

4.3.3 调试工具

# 检查插件配置
curl http://localhost:8001/plugins/{plugin_id}

# 查看Kong错误日志
tail -f /usr/local/kong/logs/error.log

# 启用调试模式
export KONG_LOG_LEVEL=debug
kong restart

5. 插件最佳实践

5.1 性能优化

5.1.1 缓存使用

-- 使用Kong缓存
local cache_key = "my_plugin:" .. consumer_id
local cached_data, err = kong.cache:get(cache_key, nil, expensive_function, arg1, arg2)

if err then
  kong.log.err("Cache error: ", err)
end

5.1.2 避免阻塞操作

-- 使用非阻塞HTTP客户端
local http = require "resty.http"
local httpc = http.new()
httpc:set_timeout(1000)  -- 设置超时

local res, err = httpc:request_uri("http://api.example.com/validate", {
  method = "POST",
  body = json.encode(data),
  headers = {
    ["Content-Type"] = "application/json",
  }
})

5.2 错误处理

5.2.1 优雅降级

function MyPluginHandler:access(conf)
  local ok, err = pcall(function()
    -- 主要逻辑
    self:validate_request(conf)
  end)
  
  if not ok then
    kong.log.err("Plugin error: ", err)
    -- 降级处理,不阻断请求
    if conf.fail_open then
      return  -- 继续处理请求
    else
      return kong.response.exit(500, { message = "Internal error" })
    end
  end
end

5.2.2 输入验证

function MyPluginHandler:validate_input(data)
  if not data or type(data) ~= "table" then
    error("Invalid input data")
  end
  
  if not data.api_key or #data.api_key < 10 then
    error("Invalid API key")
  end
  
  return true
end

5.3 安全考虑

5.3.1 敏感数据处理

-- 不要在日志中记录敏感信息
local safe_config = {
  timeout = conf.timeout,
  enabled = conf.enabled,
  -- 不包含api_key等敏感信息
}
kong.log.info("Plugin config: ", json.encode(safe_config))

5.3.2 输入清理

function MyPluginHandler:sanitize_input(input)
  if type(input) ~= "string" then
    return nil
  end
  
  -- 移除危险字符
  input = input:gsub("[<>\"'&]", "")
  
  -- 限制长度
  if #input > 1000 then
    input = input:sub(1, 1000)
  end
  
  return input
end

6. 插件测试

6.1 单元测试

6.1.1 测试框架

-- spec/my-plugin/01-unit_spec.lua
local PLUGIN_NAME = "my-plugin"
local schema = require("kong.plugins." .. PLUGIN_NAME .. ".schema")

describe(PLUGIN_NAME .. ": (schema)", function()
  it("accepts valid configuration", function()
    local ok, err = schema:validate({
      message = "Hello World",
      timeout = 5000,
      enabled = true
    })
    assert.truthy(ok)
    assert.is_nil(err)
  end)
  
  it("rejects invalid configuration", function()
    local ok, err = schema:validate({
      timeout = -1  -- 无效值
    })
    assert.falsy(ok)
    assert.not_nil(err)
  end)
end)

6.1.2 集成测试

-- spec/my-plugin/02-integration_spec.lua
local helpers = require "spec.helpers"

for _, strategy in helpers.each_strategy() do
  describe(PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function()
    local client
    
    lazy_setup(function()
      local bp = helpers.get_db_utils(strategy, nil, { PLUGIN_NAME })
      
      local route1 = bp.routes:insert({
        hosts = { "test1.com" },
      })
      
      bp.plugins:insert {
        name = PLUGIN_NAME,
        route = { id = route1.id },
        config = {
          message = "Test Message"
        },
      }
      
      assert(helpers.start_kong({
        database   = strategy,
        nginx_conf = "spec/fixtures/custom_nginx.template",
        plugins = "bundled," .. PLUGIN_NAME,
      }))
    end)
    
    lazy_teardown(function()
      helpers.stop_kong(nil, true)
    end)
    
    before_each(function()
      client = helpers.proxy_client()
    end)
    
    after_each(function()
      if client then client:close() end
    end)
    
    describe("request", function()
      it("adds custom header", function()
        local r = client:get("/request", {
          headers = {
            host = "test1.com"
          }
        })
        
        assert.response(r).has.status(200)
        assert.response(r).has.header("X-My-Plugin")
      end)
    end)
  end)
end

6.2 运行测试

# 安装测试依赖
luarocks install busted
luarocks install luacov

# 运行测试
busted spec/

# 运行特定测试
busted spec/my-plugin/

# 生成覆盖率报告
busted --coverage spec/
luacov

7. 总结

Kong的插件系统是其最强大的特性之一,通过插件可以轻松扩展Kong的功能。本章介绍了插件的基本概念、内置插件的使用、自定义插件的开发流程,以及插件的管理和测试方法。

在下一章节中,我们将详细介绍Kong的API网关配置和路由管理。