添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Kong 是一款基于 OpenResty (Nginx + Lua模块)编写的高可用、易扩展的,由 Mashape 公司开源的 API Gateway 项目。Kong 是基于 Nginx 和 Apache Cassandra 或 PostgreSQL 构建的,能提供易于使用的 RESTful API 来操作和配置 API 管理系统,所以它可以水平扩展多个 Kong 服务器,通过前置的负载均衡配置把请求均匀地分发到各个 Server,来应对大批量的网络请求

Kong 采用插件机制进行功能定制,插件集(可以是0或N个)在 API 请求响应循环的生命周期中被执行。插件使用 Lua 编写,目前已有一些基础功能:HTTP基本认证、密钥认证、CORS(Cross-Origin Resource Sharing,跨域资源共享)、TCP、UDP、文件日志、API请求限流、请求转发以及Nginx监控。

推荐查看 官网文档 ,详细了解,这里不再赘述,我们这次还是以 Kong 网关插件开发为主题,定制一个 Signature验签 插件

接下来我们首先构建一个基础环境~~~

openresty-1.13.6.2

luarocks-3.1.2

kong-1.1.2

postgresql-9.6

建议选择源码编译环境进行开发 ,如果仅仅需要运行环境,可以采用 如下 Docker 环境,以下示例统一添加了 new 标识,以区分 源码编译环境,可以自行去除(包括账号密码,自行修改)

1. create network
1
docker network create kong-net-new
2. Kong 数据存储 postgresql 构建
1
2
3
4
5
6
7
docker run -d -p 5432:5432 \
--network kong-net-new \
-e "POSTGRES_USER=kong-new" \
-e "POSTGRES_DB=kong-new" \
-e "POSTGRES_PASSWORD=kong-new" \
--name kong-database-new \
postgres:9.6

这里需要注意,为了后续的操作,需要为 postgresql 开启远程调用权限,在上述容器中 开启配置

1
sed '84 a\host\tall\t\tall\t\tall\ttrust' /var/lib/postgresql/data/pg_hba.conf
3. 构建 Konga UI平台 数据存储( 可选
1
2
3
4
5
6
7
8
docker run -d -p 3306:3306 \
--network kong-net-new \
-e "MYSQL_ROOT_PASSWORD=konga-password" \
-e "MYSQL_USER=konga-new" \
-e "MYSQL_PASS=konga-new" \
-e "MYSQL_DATABASE=konga_database_new" \
--name konga-database-new \
mysql:5.7
4. 构建 Kong

Kong 构建前的数据准备

1
2
3
4
5
6
7
8
docker run --rm \
--network kong-net-new \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database-new" \
-e "KONG_PG_DATABASE=kong-new" \
-e "KONG_PG_USER=kong-new" \
-e "KONG_PG_PASSWORD=kong-new" \
kong kong migrations bootstrap

构建 Kong 容器

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run -d -p 8000:8000 \
-p 8443:8443 \
-p 8001:8001 \
-p 8444:8444 \
--network kong-net-new \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database-new" \
-e "KONG_PG_DATABASE=kong-new" \
-e "KONG_PG_USER=kong-new" \
-e "KONG_PG_PASSWORD=kong-new" \
-e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
--name kong-new \
kong:latest

6. 构建 Konga UI 平台

数据准备

1
2
3
4
5
6
7
8
9
10
docker run -d --rm \
--network kong-net-new \
-e "DB_ADAPTER=mysql" \
-e "DB_HOST=konga-database-new" \
-e "DB_PORT=3306" \
-e "DB_USER=root" \
-e "DB_PASSWORD=konga-password" \
-e "DB_DATABASE=konga_database_new" \
-e "NODE_ENV=development" \
pantsel/konga

构建 Konga
1
2
3
4
5
6
7
8
9
10
11
12
docker run -d -p 1337:1337 \
--network kong-net-new \
-e "TOKEN_SECRET=wangpingping" \
-e "DB_ADAPTER=mysql" \
-e "DB_HOST=konga-database-new" \
-e "DB_PORT=3306" \
-e "DB_USER=root" \
-e "DB_PASSWORD=konga-password" \
-e "DB_DATABASE=konga_database_new" \
-e "NODE_ENV=production" \
--name konga-new \
pantsel/konga

signature 插件设计

参数的 签名验证 比较简单

  • 根据 client 传递的 apikey 拿到彼此约定的 apisecret 秘钥
  • 对 client 参数根据 key 进行排序,并拼接参数值
  • 加密字符串并与 client 的字符串对比
  • 插件结构

    查看如下目录结构,其中 apikeys.lua 为当前插件自定义的 逻辑提取模块,属于 插件的业务逻辑,跟 插件标准结构 没有太大关联

    1
    2
    3
    4
    5
    6
    7
    8
    9
    wettper-sign
    |-- api.lua
    |-- apikeys.lua
    |-- daos.lua
    |-- handler.lua
    |-- migrations
    | |-- 000_base_sign.lua
    | `-- init.lua
    |-- schema.lua

    核心模块说明如下

    我们接下来对每个模块进行说明:

    插件核心处理模块 handler.lua
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    -- load library
    local BasePlugin = require "kong.plugins.base_plugin"
    local constants = require "kong.constants"
    local tablex = require "pl.tablex"
    local apikeys = require "kong.plugins.wettper-sign.apikeys"
    local json = require "cjson"
    local setmetatable = setmetatable
    local concat = table.concat
    local kong = kong
    local ngx = ngx
    local ngx_log = ngx.log
    local NGX_DEBUG = ngx.DEBUG
    local string = string
    local fmt = string.format
    local EMPTY = tablex.readonly {}
    local mt_cache = { __mode = "k" }
    local config_cache = setmetatable({}, mt_cache)
    -- RESPONSE info
    local UNAUTHSIGN = 401
    local UNAUTHSIGN_CODE_FAIL = 401000
    local UNAUTHSIGN_MEG_FAIL = "sign check fail"
    local UNAUTHSIGN_MEG_NOFOUND = "secret is not found"
    -- customize header
    local WETTPERSIGNCHECK = "WETTPER-SignCheck"
    local WETTPERSIGNCHECK_FAL = "not allowed"
    local WETTPERSIGNCHECK_SUC = "allowed"
    -- load this plugin`s handler module
    local WETTPERSIGNHandler = BasePlugin:extend()
    -- execution priority of plugins
    WETTPERSIGNHandler.PRIORITY = 950
    WETTPERSIGNHandler.VERSION = "1.0.0"
    function WETTPERSIGNHandler:new()
    WETTPERSIGNHandler.super.new(self, "wettper-sign")
    end
    -- sort and join all params
    local function sort_join_param(args)
    local keys ={}
    local param_str = ""
    for i in pairs(args) do
    table.insert(keys, i)
    end
    table.sort(keys)
    for i,v in pairs(keys) do
    param_str = string.format("%s%s", param_str, tostring(args[v]))
    end
    return param_str
    end
    -- encryption parameter by md5
    local function signature(param_str, path, secret, apitime)
    local raw_string = nil
    if param_str == nil or type(param_str) == nil then
    raw_string = string.format("%s%s%s", path, secret, apitime)
    else
    raw_string = string.format("%s%s%s%s", param_str, path, secret, apitime)
    end
    local sign = ngx.md5(raw_string);
    return sign
    end
    -- main of hander.lua
    function WETTPERSIGNHandler:access(conf)
    WETTPERSIGNHandler.super.access(self)
    -- init customize header
    kong.response.set_header(WETTPERSIGNCHECK, WETTPERSIGNCHECK_FAL)
    -- get verification data from header
    local accept_apikey = kong.request.get_header("Accept-ApiKey")
    local accept_apisign = kong.request.get_header("Accept-ApiSign")
    local accept_apitime = kong.request.get_header("Accept-ApiTime")
    if accept_apikey == nil or accept_apisign == nil or accept_apitime == nil then
    kong.response.exit(UNAUTHSIGN, { message = UNAUTHSIGN_MEG_FAIL, code = UNAUTHSIGN_CODE_FAIL })
    end
    -- get secret from pgsql storage by apikey
    local raw_secrets = apikeys.get_apikeys_secret_raw(accept_apikey);
    if raw_secrets == nil or raw_secrets.secret == nil then
    kong.response.exit(UNAUTHSIGN, { message = UNAUTHSIGN_MEG_NOFOUND, code = UNAUTHSIGN_CODE_FAIL })
    end
    local path = kong.request.get_path()
    if path ~= '/' then
    path = string.sub(path, 2)
    end
    local request_method = ngx.var.request_method
    local args = nil
    local param_str = nil
    if "GET" == request_method then
    args = ngx.req.get_uri_args()
    param_str = sort_join_param(args)
    else
    ngx.req.read_body()
    local body_str = ngx.req.get_body_data()
    if body_str == nil or type(body_str) == nil then
    else
    args = json.decode(body_str)
    param_str = sort_join_param(args)
    end
    end
    -- encryption parameter by md5
    local sign = signature(param_str, path, raw_secrets.secret, accept_apitime)
    if sign ~= accept_apisign then
    kong.response.exit(UNAUTHSIGN, { message = UNAUTHSIGN_MEG_FAIL, code = UNAUTHSIGN_CODE_FAIL })
    end
    kong.response.set_header(WETTPERSIGNCHECK, WETTPERSIGNCHECK_SUC)
    end
    return WETTPERSIGNHandler
    APIKEY 处理模块 apikeys.lua
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    -- load library
    local tablex = require "pl.tablex"
    local EMPTY = tablex.readonly {}
    local kong = kong
    local type = type
    local mt_cache = { __mode = "k" }
    local setmetatable = setmetatable
    local consumer_apikeys_cache = setmetatable({}, mt_cache)
    local consumer_in_apikeys_cache = setmetatable({}, mt_cache)
    local apikeys_secret_cache = setmetatable({}, mt_cache)
    local function load_secret_into_memory(apikey)
    local secrets, err = kong.db.wettpersign_credentials:select_by_apikey(apikey)
    if err or secrets == nil then
    return nil, err
    end
    return secrets
    end
    local function get_apikeys_secret_raw(apikey)
    local cache_key = kong.db.wettpersign_credentials:cache_key(apikey)
    local raw_secret, err = kong.cache:get(cache_key, nil, load_secret_into_memory, apikey)
    if err then
    return nil, err
    end
    -- use EMPTY to be able to use it as a cache key, since a new table would
    -- immediately be collected again and not allow for negative caching.
    return raw_secret or EMPTY
    end
    local function load_apikeys_into_memory(consumer_pk)
    local apikeys = {}
    local len = 0
    for row, err in kong.db.wettpersign_credentials:each_for_consumer(consumer_pk, 1000) do
    if err then
    return nil, err
    end
    len = len + 1
    apikeys[len] = row
    end
    return apikeys
    end
    --- Returns the database records with apikeys the consumer belongs to
    -- @param consumer_id (string) the consumer for which to fetch the apikeys it belongs to
    -- @return table with apikey records (empty table if none), or nil+error
    local function get_consumer_apikeys_raw(consumer_id)
    local cache_key = kong.db.wettpersign_credentials:cache_key(consumer_id)
    local raw_apikeys, err = kong.cache:get(cache_key, nil, load_apikeys_into_memory, { id = consumer_id })
    if err then
    return nil, err
    end
    -- use EMPTY to be able to use it as a cache key, since a new table would
    -- immediately be collected again and not allow for negative caching.
    return raw_apikeys or EMPTY
    end
    --- Returns a table with all apikey names a consumer belongs to.
    -- The table will have an array part to iterate over, and a hash part
    -- where each apikey name is indexed by itself. Eg.
    -- {
    -- [1] = "users",
    -- [2] = "admins",
    -- users = "users",
    -- admins = "admins",
    -- }
    -- If there are no apikeys defined, it will return an empty table
    -- @param consumer_id (string) the consumer for which to fetch the apikeys it belongs to
    -- @return table with apikeys (empty table if none) or nil+error
    local function get_consumer_apikeys(consumer_id)
    local raw_apikeys, err = get_consumer_apikeys_raw(consumer_id)
    if not raw_apikeys then
    return nil, err
    end
    local apikeys = consumer_apikeys_cache[raw_apikeys]
    if not apikeys then
    apikeys = {}
    consumer_apikeys_cache[raw_apikeys] = apikeys
    for i = 1, #raw_apikeys do
    local apikey = raw_apikeys[i].apikey
    apikeys[i] = apikey
    apikeys[apikey] = apikey
    end
    end
    return apikeys
    end
    --- checks whether a consumer-apikey-list is part of a given list of apikeys.
    -- @param apikeys_to_check (table) an array of apikey names. Note: since the
    -- results will be cached by this table, always use the same table for the
    -- same set of apikeys!
    -- @param consumer_apikeys (table) list of consumer apikeys (result from
    -- `get_consumer_apikeys`)
    -- @return (boolean) whether the consumer is part of any of the apikeys.
    local function consumer_in_apikeys(apikeys_to_check, consumer_apikeys)
    -- 1st level cache on "apikeys_to_check"
    local result1 = consumer_in_apikeys_cache[apikeys_to_check]
    if result1 == nil then
    result1 = setmetatable({}, mt_cache)
    consumer_in_apikeys_cache[apikeys_to_check] = result1
    end
    -- 2nd level cache on "consumer_apikeys"
    local result2 = result1[consumer_apikeys]
    if result2 ~= nil then
    return result2
    end
    -- not found, so validate and populate 2nd level cache
    result2 = false
    for i = 1, #apikeys_to_check do
    if consumer_apikeys[apikeys_to_check[i]] then
    result2 = true
    break
    end
    end
    result1[consumer_apikeys] = result2
    return result2
    end
    --- Gets the currently identified consumer for the request.
    -- Checks both consumer and if not found the credentials.
    -- @return consumer_id (string), or alternatively `nil` if no consumer was
    -- authenticated.
    local function get_current_consumer_id()
    return (kong.client.get_consumer() or EMPTY).id or (kong.client.get_credential() or EMPTY).consumer_id
    end
    --- Returns a table with all apikey names.
    -- The table will have an array part to iterate over, and a hash part
    -- where each apikey name is indexed by itself. Eg.
    -- {
    -- [1] = "users",
    -- [2] = "admins",
    -- users = "users",
    -- admins = "admins",
    -- }
    -- If there are no authenticated_apikeys defined, it will return nil
    -- @return table with apikeys or nil
    local function get_authenticated_apikeys()
    local authenticated_apikeys = kong.ctx.shared.authenticated_apikeys
    if type(authenticated_apikeys) ~= "table" then
    authenticated_apikeys = ngx.ctx.authenticated_apikeys
    if authenticated_apikeys == nil then
    return nil
    end
    if type(authenticated_apikeys) ~= "table" then
    kong.log.warn("invalid authenticated_apikeys, a table was expected")
    return nil
    end
    end
    local apikeys = {}
    for i = 1, #authenticated_apikeys do
    apikeys[i] = authenticated_apikeys[i]
    apikeys[authenticated_apikeys[i]] = authenticated_apikeys[i]
    end
    return apikeys
    end
    --- checks whether a apikey-list is part of a given list of apikeys.
    -- @param apikeys_to_check (table) an array of apikey names.
    -- @param apikeys (table) list of apikeys (result from
    -- `get_authenticated_apikeys`)
    -- @return (boolean) whether the authenticated apikey is part of any of the
    -- apikeys.
    local function apikey_in_apikeys(apikeys_to_check, apikeys)
    for i = 1, #apikeys_to_check do
    if apikeys[apikeys_to_check[i]] then
    return true
    end
    end
    end
    return {
    get_current_consumer_id = get_current_consumer_id,
    get_consumer_apikeys = get_consumer_apikeys,
    get_authenticated_apikeys = get_authenticated_apikeys,
    consumer_in_apikeys = consumer_in_apikeys,
    apikey_in_apikeys = apikey_in_apikeys,
    get_apikeys_secret_raw = get_apikeys_secret_raw,
    }
    配置以及 Admin API 模块

    schema.lua

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    -- load library
    local typedefs = require "kong.db.schema.typedefs"
    -- plugin configuration constraints
    return {
    name = "wettper-sign",
    fields = {
    { consumer = typedefs.no_consumer },
    { run_on = typedefs.run_on_first },
    { config = {
    type = "record",
    fields = {
    --{ encry_method = { type = "string", default = "md5" }, },
    { header_apikey = { type = "string", default = "Accept-ApiKey" }, },
    { header_apisign = { type = "string", default = "Accept-ApiSign" }, },
    { header_apitime = { type = "string", default = "Accept-ApiTime" }, },
    }
    }
    }
    },
    }

    daos.lua

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    -- load library
    local typedefs = require "kong.db.schema.typedefs"
    return {
    wettpersign_credentials = {
    name = "wettpersign_credentials",
    primary_key = { "id" },
    endpoint_key = "apikey",
    cache_key = { "apikey" },
    fields = {
    { id = typedefs.uuid },
    { created_at = typedefs.auto_timestamp_s },
    { consumer = { type = "foreign", reference = "consumers", default = ngx.null, on_delete = "cascade", }, },
    { apikey = { type = "string", unique = true, required = false, auto = true } },
    { secret = { type = "string", required = true } },
    },
    },
    }

    api.lua Admin API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    -- load library
    local endpoints = require "kong.api.endpoints"
    local kong = kong
    local wettpersign_schema = kong.db.wettpersign_credentials.schema
    local consumers_schema = kong.db.consumers.schema
    return {
    -- operating a single piece of data
    ["/wettper-sign/:wettpersign_credentials"] = {
    schema = wettpersign_schema,
    methods = {
    before = function(self, db, helpers)
    if self.req.method ~= "PUT" then
    local wettpersign, _, err_t = endpoints.select_entity(self, db, wettpersign_schema)
    if err_t then
    return endpoints.handle_error(err_t)
    end
    if not wettpersign then
    return kong.response.exit(HTTP_NOT_FOUND, { message = "Not found" })
    end
    self.wettpersign_credential = wettpersign
    self.params.wettpersign_credential = wettpersign.apikey
    end
    end,
    GET = endpoints.get_entity_endpoint(wettpersign_schema),
    PUT = function(self, db, helpers)
    self.args.post.apikey = { apikey = self.params.wettpersign_credential }
    return endpoints.put_entity_endpoint(wettpersign_schema)(self, db, helpers)
    end,
    DELETE = endpoints.delete_entity_endpoint(wettpersign_schema),
    },
    },
    -- get all data or create new data
    ["/wettper-sign"] = {
    schema = wettpersign_schema,
    methods = {
    GET = endpoints.get_collection_endpoint(wettpersign_schema),
    POST = endpoints.post_collection_endpoint(wettpersign_schema),
    }
    },
    }

    migrations 数据

    migrations/init.lua

    1
    2
    3
    return {
    "000_base_wettpersign",
    }

    migrations/000_base_wettpersign.lua

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    return {
    postgres = {
    up = [[
    CREATE TABLE IF NOT EXISTS "wettpersign_credentials" (
    "id" uuid NOT NULL,
    "created_at" timestamptz(6) DEFAULT timezone('UTC'::text, ('now'::text)::timestamp(0) with time zone),
    "consumer_id" uuid,
    "apikey" text COLLATE "pg_catalog"."default",
    "secret" text COLLATE "pg_catalog"."default",
    CONSTRAINT "wettpersign_credentials_pkey" PRIMARY KEY ("id"),
    CONSTRAINT "wettpersign_credentials_consumer_id_fkey" FOREIGN KEY ("consumer_id") REFERENCES "consumers" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
    CONSTRAINT "wettpersign_credentials_apikey" UNIQUE ("apikey")
    );
    CREATE INDEX IF NOT EXISTS "wettpersign_credentials_consumer_id_idx" ON "wettpersign_credentials" USING btree (
    "consumer_id" "pg_catalog"."uuid_ops" ASC NULLS LAST
    );
    ]],
    },
    cassandra = {
    up = [[
    ]],
    },
    }

    插件部署

    此次主要以 源码包 安装为例,后续再对 luarocks 包安装进行说明

  • 插件放入 plugins 目录下,默认在 /usr/local/share/lua/5.1/kong/plugins/

  • 配置修改,添加所需插件,默认配置 path 为 /etc/kong/kong.conf

    1
    plugins = bundled,wettper-sign
  • 插件数据构建(如果需要)

    1
    {kong 安装路径}/bin/kong migrations up
  • 重启 Kong,插件的 安装需要 重启,普通配置修改只需要 reload

    1
    {kong 安装路径}/bin/kong restart -c {kong 配置文件路径}/kong.conf —vv
    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置: jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true
  •