Kong 通过强大的插件机制进行了类似AOP切面的方式操作,提供了日志、认证、限流等操作。官方给出了许多成熟插件可供选择,包括:
- 认证:Basic Authentication、Key Authentication、OAuth 2.0 Authentication、JWT、LDAP Authentication
- 安全:ACL、CORS、Dynamic SSL、IP Restriction、Bot Detection
- 传输控制:Request Size Limiting、Rate Limiting、Response Rate Limiting、Request Termination、
- 分析监控:Prometheus、Zipkin、Datadog
- 日志:TCP Log、UDP Log、HTTP Log、File Log、StatsD、Syslog、Loggly
- 请求响应调整:Request Transformer、Response Transformer、Correlation ID
- Serverless:Serverless Functions、Azure Functions、AWS Lambda、 Apache OpenWhisk
下面从官网的例子 key-auth 插件来看具体使用
插件使用
首先执行命令给 Service 添加一个 key-value 插件。此处假设已经存在了一个名为 baidu-service 的服务,具体服务的创建参见Kong Api网关简介(一) 安装运行
命令行执行命令:
$ curl -i -X POST \
> --url http://127.0.0.1:8001/services/baidu-service/plugins/ \
> --data 'name=key-auth'
HTTP/1.1 201 Created
Date: Mon, 09 Jul 2018 06:53:31 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.14.0
Content-Length: 276
{"created_at":1531119211000,"config":{"key_in_body":false,"run_on_preflight":true,"anonymous":"","hide_credentials":false,"key_names":["apikey"]},"id":"4d08e1fd-fb3f-4e5a-8a11-e20ad49c9f4b","enabled":true,"service_id":"edbe2f0f-f11e-493b-9b59-5997e50c3de1","name":"key-auth"}
可以通过管理API查看插件的其他管理操作,具体参见Kong Admin API Plugin Object
这时候再执行上一篇文章中执行的命令访问 8000 端口的接口地址,显示找不到 API key,说明 key-auth 插件已经起作用:
$ curl -i -X GET \
> --url http://127.0.0.1:8000/ \
> --header 'Host: baidu.com'
HTTP/1.1 401 Unauthorized
Date: Mon, 09 Jul 2018 08:47:10 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
WWW-Authenticate: Key realm="kong"
Server: kong/0.14.0
Content-Length: 41
{"message":"No API key found in request"}
为了继续测试,我们创建一个consumer,执行命令:
[xueyufish@izbp13cqwumhn3wzp2j5mqz kong-gateway]$ curl -i -X POST \
> --url http://127.0.0.1:8001/consumers/ \
> --data "username=xueyufish"
HTTP/1.1 201 Created
Date: Mon, 09 Jul 2018 08:52:13 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.14.0
Content-Length: 110
{"custom_id":null,"created_at":1531126333,"username":"xueyufish","id":"580046a0-bbf0-4b9f-91ad-9324976df6be"}
Kong 中的消费者对象代表了一个服务的消费者或者一个用户。我们可以使用 Kong 作为消费者数据存储,或者也可以将用户列表映射到数据库,以保持 Kong 与现有主数据存储的一致性;在BFF模型下,也可以为每个业务实现,或者client定义一个消费者,例如:ios、android、pc,或者针对具体不同的业务实现具备不同的消费者,具体根据业务需要而定。
然后,执行命令给创建的消费者添加一个key:
$ curl -i -X POST \
> --url http://127.0.0.1:8001/consumers/xueyufish/key-auth/ \
> --data 'key=123456'
HTTP/1.1 201 Created
Date: Mon, 09 Jul 2018 09:05:59 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.14.0
Content-Length: 141
{"id":"b18b0c03-4632-4ca5-8f0c-814d04da1022","created_at":1531127160000,"key":"123456","consumer_id":"580046a0-bbf0-4b9f-91ad-9324976df6be"}
这时候在访问的请求header中带上apiKey参数,可以发现能够成功访问:
$ curl -i -X GET \
> --url http://127.0.0.1:8000 \
> --header "Host: baidu.com" \
> --header "apikey: 123456"
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 2381
Connection: keep-alive
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Date: Mon, 09 Jul 2018 09:09:15 GMT
Etag: "588604c8-94d"
Last-Modified: Mon, 23 Jan 2017 13:27:36 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
X-Kong-Upstream-Latency: 54
X-Kong-Proxy-Latency: 33
Via: kong/0.14.0
<!DOCTYPE html>
<!--STATUS OK--><html>... 省略html内容 ...</html>
插件分析
文件结构
Kong 插件的文件结构分基本插件模块和完整插件模块两种,基本插件模块结构如下:
simple-plugin
├── handler.lua
└── schema.lua
其中,handler.lua 是插件核心,它是一个接口实现,其中每个函数将在请求生命周期中的期望时刻运行。schema.lua 用于定义插件配置
完整插件模块结构如下:
complete-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│ ├── cassandra.lua
│ └── postgres.lua
└── schema.lua
其中,api.lua 定义管理API操作接口;daos.lua 定义插件需要并且存储在数据库的实体的DAOs列表;migrations/*.lua 定义了给定数据存储的相应迁移,通常只有当插件必须在数据库中存储自定义实体并通过daos.lua定义的DAO进行交互时,迁移才是必要的。
具体关于文件结构的描述参见Plugin Development - File Structure
插件配置
Kong 插件通过schema.lua文件定义配置。schema.lua 返回一个Table类型,包含no_consumer、fields、self_check三个属性:
属性名 | Lua 类型 | 默认值 | 描述 |
---|---|---|---|
no_consumer | Boolean | false | 如果为true 将不能应用此插件至指定消费者,只能被应用到 Services 或者 Routes, 例如:认证插件 |
fileds | Table | {} | 插件的 schema,使用一个键值对定义可用属性和他们的规则 |
self_check | Function | nil | 如果在接受插件配置之前需要进行自定义验证,需要实现此函数 |
schema.lua 文件样本如下:
return {
no_consumer = true, -- this plugin will only be applied to Services or Routes,
fields = {
-- Describe your plugin's configuration's schema here.
},
self_check = function(schema, plugin_t, dao, is_updating)
-- perform any custom verification
return true
end
}
更多具体配置参见 Plugin Development - Store Configuration
从 key-auth 插件的 schema 文件可以看出,定义了五个 field, 其中刚用到的 key_name 默认为必填,类型为 array, 默认值为一个名为default_key_names的function,同时附加了 check_keys function 用于验证header。
local function check_keys(keys)
for _, key in ipairs(keys) do
local res, err = utils.validate_header_name(key, false)
if not res then
return false, "'" .. key .. "' is illegal: " .. err
end
end
return true
end
local function default_key_names(t)
if not t.key_names then
return { "apikey" }
end
end
return {
no_consumer = true,
fields = {
key_names = {required = true, type = "array", default = default_key_names, func = check_keys},
hide_credentials = {type = "boolean", default = false},
...
}
}
逻辑实现
Kong 插件可以在请求/响应生命周期中的几个入口点注入自定义逻辑,插件实现者必须在 handler.lua 中实现 base_plugin.lua 文件中的一个或多个接口。 base_plugin.lua 文件中的几个方法如下:
函数名 | LUA-NGINX-MODULE Context | 描述 |
---|---|---|
:init_worker() | init_worker_by_lua | 在每个 Nginx 工作进程启动时执行 |
:certificate() | ssl_certificate_by_lua | 在SSL握手阶段的SSL证书服务阶段执行 |
:rewrite() | rewrite_by_lua | 从客户端接收作为重写阶段处理程序的每个请求执行。在这个阶段,无论是API还是消费者都没有被识别,因此这个处理器只在插件被配置为全局插件时执行 |
:access() | access_by_lua | 为客户的每一个请求而执行,并在它被代理到上游服务之前执行 |
:header_filter() | header_filter_by_lua | 从上游服务接收到所有响应头字节时执行 |
:body_filter() | body_filter_by_lua | 从上游服务接收的响应体的每个块时执行。由于响应流回客户端,它可以超过缓冲区大小,因此,如果响应较大,该方法可以被多次调用 |
:log() | log_by_lua | 当最后一个响应字节已经发送到客户端时执行 |
key-auth 插件的 handler.lua 文件实现了其中的 access() 方法:
function KeyAuthHandler:access(conf)
KeyAuthHandler.super.access(self)
-- check if preflight request and whether it should be authenticated
if not conf.run_on_preflight and get_method() == "OPTIONS" then
return
end
if ngx.ctx.authenticated_credential and conf.anonymous ~= "" then
-- we're already authenticated, and we're configured for using anonymous,
-- hence we're in a logical OR between auth methods and we're already done.
return
end
local ok, err = do_authentication(conf)
if not ok then
if conf.anonymous ~= "" then
-- get anonymous user
local consumer_cache_key = singletons.dao.consumers:cache_key(conf.anonymous)
local consumer, err = singletons.cache:get(consumer_cache_key, nil,
load_consumer,
conf.anonymous, true)
if err then
responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
set_consumer(consumer, nil)
else
return responses.send(err.status, err.message)
end
end
end
除此之外,Kong 还提供了数据访问、插件缓存、管理API等可以自定义功能,具体参见Kong Plugin Development
自定义插件
下面创建一个 hello-world 自定义插件,插件本身并没有太大意义,只为了演示流程。
准备
为了更方便测试,先卸载之前镜像,删除本地的postgres数据库重新pull。
$ docker-compose down
$ sudo rm -rf postgresql/*
在 kong-gateway/plugins目录下添加两个文件 handler.lua 和 schema.lua,内容如下:
-- handler.lua
local BasePlugin = require "kong.plugins.base_plugin"
local HelloWorldHandler = BasePlugin:extend()
local ngx_log = ngx.log
local DEBUG = ngx.DEBUG
HelloWorldHandler.PRIORITY = 3000
function HelloWorldHandler:new()
HelloWorldHandler.super.new(self, "hello-world")
end
function HelloWorldHandler:access(conf)
HelloWorldHandler.super.access(self)
if conf.say_hello then
ngx_log(DEBUG, "============ Hello World! ============")
ngx.header["Hello-World"] = "=== Hello World ==="
else
ngx_log(DEBUG, "============ Bye World! ============")
ngx.header["Hello-World"] = "=== Bye World ==="
end
end
return HelloWorldHandler
-- schema.lua
return {
no_consumer = true,
fields = {},
self_check = function(schema, plugin_t, dao, is_updating)
return true
end
}
同时,在 kong-gateway/kong.conf 配置文件中增加插件配置:
plugins = hello-world
此时目录结构如下:
kong-gateway
| -- config
| -- kong.conf
| -- plugins
| -- hello-world
| -- handler.lua
| -- schema.lua
| -- postgresql
| -- docker-compose.yml
由于在 docker-compose.yml 中指定了 plugins 目录的挂载点为 /etc/kong/plugins,此时可以查看刚添加的 hello-world 目录已经被挂载到 docker 容器中:
$ docker-compose exec kong ls -la /etc/kong/plugins/hello-world
total 16
drwxrwxr-x 2 1000 1000 4096 Jul 10 02:38 .
drwxrwxr-x 3 1000 1000 4096 Jul 10 02:38 ..
-rw-r--r-- 1 1000 1000 822 Jul 10 02:04 handler.lua
-rw-r--r-- 1 1000 1000 345 Jul 6 09:25 schema.lua
重新迁移数据库,启动 kong:
$ docker-compose run kong kong migrations up
$ docker-compose up --no-recreate –d
容器启动以后,根据Kong Api网关简介(一) 安装运行中的内容添加Service、Route,再添加插件:
$ curl -i -X POST \
> --url http://127.0.0.1:8001/plugins/ \
> --data 'name=hello-world'
HTTP/1.1 201 Created
Date: Tue, 10 Jul 2018 06:51:12 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.14.0
Content-Length: 137
{"created_at":1531205473000,"config":{"say_hello":true},"id":"bf733b5a-c49f-46b3-a680-a7e3a75414ac","enabled":true,"name":"hello-world"}
运行
上述完成以后,终端执行curl进行测试:
$ curl -i -X GET \
> --url http://localhost:8000/ \
> --header 'Host: baidu.com'
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 2381
Connection: keep-alive
Hello-World: === Hello World ===
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Date: Tue, 10 Jul 2018 07:01:31 GMT
Etag: "588604c8-94d"
Last-Modified: Mon, 23 Jan 2017 13:27:36 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
X-Kong-Upstream-Latency: 62
X-Kong-Proxy-Latency: 9
Via: kong/0.14.0
<!DOCTYPE html>
<!--STATUS OK--><html>省略 html</html>
可以看见 header 中增加了 Hello-world
内容,说明插件生效了。