Kong 开发自定义Auth认证插件

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本文主要介绍kong 网关插件的编写,所用到的库,以及插件文件建结构等,测试用例采用开发一个简单的自定义权限认证插件。参考官网地址为

1. 文件结构介绍

  1. 一个简单的插件的文件结构如下:
simple-plugin
├── handler.lua
└── schema.lua
复制代码
  1. 一个功能比较复杂的插件文件结构如下:
complete-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│   ├── init.lua
│   └── 000_base_complete_plugin.lua
└── schema.lua

复制代码
  1. 字段解释
名字 是否必填 描述
api.lua 定义一个可以再UI管理界面调用的list,用以与插件实体本省进行交互。
daos.lua 数据交互的相关文件,定义一些数据库交互信息,在插件使用数据库时使用。
handler.lua 插件的核心逻辑,是一个接口,实现一些再kong的request和connection中各种操作。
migrations*/*.lua 定义一些数据库脚本操作, 如创建表等,与daos.lua搭配使用。
schema.lua 配置插件所需字段配置信息,以及一些字段校验操作。

2. 用户自定义逻辑接口介绍

用户自定义逻辑,主要是handler.luaschema.lua这两个文件的一些逻辑,在开发过程中可以使用官方的模板github.com/Kong/kong-p… 进行开发,拉下代码后项目结构如下

image.png 在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. 开发自定义插件

  1. 插件思路

由konga管理界面配置请求的授权服务器,对path实现授权,其配置信息主要包括授权服务器endpoint, 请求方式,请求key 等信息

image.png

image.png

  1. 插件实现代码
    1. 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

复制代码
  1. 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两个原生插件的源码,编写的,使用的库如下:

  1. local httpUtil = require "resty.http"
  2. local table_clear = require "table.clear"
  3. local url = require "socket.url"
  4. local cjson = require "cjson"

猜你喜欢

转载自juejin.im/post/7101561475621191716
今日推荐