在前面的文章介绍了 Kong 的相关实践,本文将会介绍 Kong 的利器:插件以及自定义插件。
Kong 几种常用插件的应用
请求到达 Kong,在转发给服务端应用之前,我们可以应用 Kong 自带的插件对请求进行处理,如合法认证、限流控制、黑白名单校验和日志采集等等。同时,我们也可以按照 Kong 的教程文档,定制开发属于自己的插件。本小节将会选择其中的两个插件示例应用,其余的插件应用,可以参见:https://docs.konghq.com/hub/。
JWT 认证插件
JWT 是目前最流行的跨域身份验证解决方案。作为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的。
关于为什么使用 JWT,不在本小节详细论述,具体可见 统一认证与授权在微服务架构中的设计与实战。Kong 提供了 JWT 认证插件,用以验证包含 HS256 或 RS256 签名的 JWT 的请求(如RFC 7519中所述)。每个消费者都将拥有 JWT 凭证(公钥和密钥),这些凭证必须用于签署其 JWT。JWT 令牌可以通过请求字符串、cookie 或者认证头部传递。Kong 将会验证令牌的签名,通过则转发,否则直接丢弃请求。
我们在前面小节配置的路由基础上,增加 JWT 认证插件。
代码语言:javascript复制curl -X POST http://localhost:8001/routes/e33d6aeb-4f35-4219-86c2-a41e879eda36/plugins
--data "name=jwt"
可以看到,在插件列表增加了相应的记录。
在增加了 JWT 插件之后,就没法直接访问 /api/blog
接口了,接口返回:"message": "Unauthorized"
。提示客户端要访问需要提供 JWT 的认证信息。因此,我们需要创建用户:
curl -i -X POST
--url http://localhost:8001/consumers/
--data "username=aoho"
如上创建了一个名为 aoho 的用户。
创建好用户之后,需要获取用户 JWT 凭证,执行如下的调用:
代码语言:javascript复制curl -i -X POST
--url http://localhost:8001/consumers/aoho/jwt
--header "Content-Type: application/x-www-form-urlencoded"
// 响应
{
"rsa_public_key": null,
"created_at": 1563566125,
"consumer": {
"id": "8c0e1ab4-8411-42fc-ab80-5eccf472d2fd"
},
"id": "1d69281d-5083-4db0-b42f-37b74e6d20ad",
"algorithm": "HS256",
"secret": "olsIeVjfVSF4RuQuylTMX4x53NDAOQyO",
"key": "TOjHFM4m1qQuPPReb8BTWAYCdM38xi3C"
}
使用 key 和 secret 在 https://jwt.io
可以生成 JWT 凭证信息。在实际的使用过程中,我们通过编码实现,此处为了演示使用网页工具生成 Token。
将生成的 Token,配置到请求的认证头部,再次执行请求:
可以看到,我们能够正常请求相应的 API 接口。JWT 认证插件应用成功。
Prometheus 可视化监控
Prometheus 是一套开源的系统监控报警框架。它启发于 Google 的 borgmon 监控系统,由工作在 SoundCloud 的 google 前员工在 2012 年创建,作为社区开源项目进行开发,并于 2015 年正式发布。2016 年,Prometheus 正式加入 Cloud Native Computing Foundation,成为受欢迎度仅次于 Kubernetes 的项目。作为新一代的监控框架,Prometheus 适用于记录时间序列数据,具有强大的多维度数据模型、灵活而强大的查询语句、易于管理和伸缩等特点。
Kong 官方提供的 Prometheus 插件,可用的 metric 如下:
- 状态码:上游服务返回的 HTTP 状态码;
- 时延柱状图:Kong 中的时延都将被记录,包括如下:
- 请求:完整请求的时延;
- Kong:Kong用来路由、验证和运行其他插件所花费的时间;
- 上游:上游服务所花费时间来响应请求。
- Bandwidth:流经 Kong 的总带宽(出口/入口);
- DB 可达性:Kong 节点是否能访问其 DB;
- Connections:各种 NGINX 连接指标,如 Active、读取、写入、接受连接。
我们在 Service 为 aoho-blog 的服务上安装 Prometheus 插件:
代码语言:javascript复制curl -X POST http://localhost:8001/services/aoho-blog/plugins
--data "name=prometheus"
可以从管理界面看到,我们己经成功将 Prometheus 插件绑定到 aoho-blog 服务上。
通过访问 /metrics
接口返回收集度量数据:
$ curl -i http://localhost:8001/metrics
HTTP/1.1 200 OK
Server: openresty/1.13.6.2
Date: Sun, 21 Jul 2019 09:48:42 GMT
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
kong_bandwidth{type="egress",service="aoho-blog"} 178718
kong_bandwidth{type="ingress",service="aoho-blog"} 1799
kong_datastore_reachable 1
kong_http_status{code="200",service="aoho-blog"} 4
kong_http_status{code="401",service="aoho-blog"} 1
kong_latency_bucket{type="kong",service="aoho-blog",le="00005.0"} 1
kong_latency_bucket{type="kong",service="aoho-blog",le="00007.0"} 1
...
kong_latency_bucket{type="upstream",service="aoho-blog",le="00300.0"} 4
kong_latency_bucket{type="upstream",service="aoho-blog",le="00400.0"} 4
...
kong_latency_count{type="kong",service="aoho-blog"} 5
kong_latency_count{type="request",service="aoho-blog"} 5
kong_latency_count{type="upstream",service="aoho-blog"} 4
kong_latency_sum{type="kong",service="aoho-blog"} 409
kong_latency_sum{type="request",service="aoho-blog"} 1497
kong_latency_sum{type="upstream",service="aoho-blog"} 1047
kong_nginx_http_current_connections{state="accepted"} 2691
kong_nginx_http_current_connections{state="active"} 2
kong_nginx_http_current_connections{state="handled"} 2691
kong_nginx_http_current_connections{state="reading"} 0
kong_nginx_http_current_connections{state="total"} 2637
kong_nginx_http_current_connections{state="waiting"} 1
kong_nginx_http_current_connections{state="writing"} 1
kong_nginx_metric_errors_total 0
返回的响应太长,有省略,从响应可以看到 Prometheus 插件提供的 metric 都有体现。Prometheus 插件导出的度量标准,可以在 Grafana 中绘制,读者可以自行尝试。
链路追踪 Zipkin 插件
Zipkin 是一款开源的分布式实时数据追踪系统。其主要功能是聚集来自各个异构系统的实时监控数据,用来追踪微服务架构下的系统延时问题。应用系统需要向 Zipkin 报告数据。Kong 的 Zipkin 插件作为 zipkin-client 就是组装好 Zipkin 需要的数据包,往 Zipkin-server 发送数据。Zipkin 插件会将请求打上如下标签,并推送到 Zipkin 服务端:
- span.kind (sent to Zipkin as “kind”)
- http.method
- http.status_code
- http.url
- peer.ipv4
- peer.ipv6
- peer.port
- peer.hostname
- peer.service
关于链路追踪和 Zipkin 的具体信息,参见详解微服务架构中的全链路追踪,本次 chat 旨在介绍如何在 Kong 中使用 Zipkin 插件追踪所有请求的链路。
首先开启 Zipkin 插件,将插件绑定到路由上(这里可以绑定为全局的插件)。
代码语言:javascript复制curl -X POST http://kong:8001/routes/e33d6aeb-4f35-4219-86c2-a41e879eda36/plugins
--data "name=zipkin"
--data "config.http_endpoint=http://localhost:9411/api/v2/spans"
--data "config.sample_ratio=1"
如上配置了 Zipkin Collector 的地址和采样率,为了效果明显,设置采样率为 100%,生产环境谨慎使用,采样率对系统吞吐量有影响。
可以看到,Zipkin 插件已经应用到指定的路由上。下面我们将会执行请求 /api/blog
接口,打开 http://localhost:9411
界面如下:
Zipkin 已经将请求记录,我们可以点开查看详细的链路详情:
从链路调用可以知道,请求到达 Kong 之后,经历了哪些服务和 Span,每个 Span 所花费的时间等等信息。
自定义插件的实践
官方虽然提供了很多插件,但是我们在实际的业务场景中还会有业务的需求,定制插件能够帮助我们更好地管理 API Gateway。Kong 提供了插件开发包和示例,自定义插件只需要按照提供的步骤即可。
Kong 安装
在上面小节,笔者介绍了通过镜像的方式安装 Kong,本部分为了方便编写自定义插件,我们使用本地安装的 Kong,笔者的环境是 macOS,安装较为简单:
代码语言:javascript复制 $ brew tap kong/kong
$ brew install kong
其次安装 Postgres,并下载 kong.conf.default 配置文件(参见 https://raw.githubusercontent.com/Kong/kong/master/kong.conf.default),执行如下的命令:
代码语言:javascript复制 $ sudo mkdir -p /etc/kong
$ sudo cp kong.conf.default /etc/kong/kong.conf
执行 migration:
代码语言:javascript复制kong migrations bootstrap -c /etc/kong/kong.conf
随后即可启动 Kong:
代码语言:javascript复制kong start -c /etc/kong/kong.conf
启动之后,通过 8001 管理端口验证是否成功。
代码语言:javascript复制curl -i http://localhost:8001/
基于安装好的 Kong,我们介绍一下如何将自定义的插件加入到 Kong 的可选插件中,这里以鉴权的 token-auth 插件为例进行讲解。
Kong 官方提供了有关认证的插件有:JWT、OAuth 2.0 和 Basic Auth 等,我们在实际业务中,也经常会自建认证和授权服务器,这样就需要我们在 API 网关处拦截验证请求的合法性。基于此,我们实现一个类似 Kong 过滤器的插件:token-auth。
Kong 自带的插件在 /usr/local/share/lua/5.1/kong/plugins/
目录下。每个插件文件夹下有如下两个主要文件:
- schema.lua:定义的启动插件时的参数检查;
- handler.lua:文件定义了各阶段执行的函数,插件的核心。
token-auth 是我们定制的插件名。在 /usr/local/share/lua/5.1/kong/plugins
下新建 token-auth 目录。Plugin 的加载和初始化阶段,即 Kong.init()
在加载插件的时候,会将插件目录中的 schema.lua 和 handler.lua 加载,下面我们看下这两个脚本的实现。
插件配置定义:schema.lua
Kong 中每个插件的配置存放在 plugins 表中的 config 字段,是一段 json 文本,token-auth 所需的配置定义如下:
代码语言:javascript复制return {
no_consumer = true,
fields = {
auth_server_url = {type = "url", required = true},
}
}
从 schema.lua 可以看到,启用 token-auth 插件时,需要检查 auth_server_url 字段为 URL 类型,且不能为空。
插件功能实现:handler.lua
handler.lua 实现了插件认证功能,这个插件中定义的方法,会在处理请求和响应的时候被调用。
代码语言:javascript复制llocal http = require "socket.http"
local ltn12 = require "ltn12"
local cjson = require "cjson.safe"
local BasePlugin = require "kong.plugins.base_plugin"
local TokenAuthHandler = BasePlugin:extend()
TokenAuthHandler.PRIORITY = 1000
local KEY_PREFIX = "auth_token"
local EXPIRES_ERR = "token expires"
--- 提取 JWT 头部信息
-- @param request ngx request object
-- @return token JWT
-- @return err
local function extract_token(request)
local auth_header = request.get_headers()["authorization"]
if auth_header then
local iterator, ierr = ngx.re.gmatch(auth_header, "\s*[Bb]earer\s (. )")
if not iterator then
return nil, ierr
end
local m, err = iterator()
if err then
return nil, err
end
if m and #m > 0 then
return m[1]
end
end
end
--- 调用 auth server 验证 token 合法性
-- @param token Token to be validated
-- @param conf Plugin configuration
-- @return info Information associated with token
-- @return err
local function query_and_validate_token(token, conf)
ngx.log(ngx.DEBUG, "get token info from: ", conf.auth_server_url)
local response_body = {}
local res, code, response_headers = http.request{
url = conf.auth_server_url,
method = "GET",
headers = {
["Authorization"] = "bearer " .. token
},
sink = ltn12.sink.table(response_body),
}
if type(response_body) ~= "table" then
return nil, "Unexpected response"
end
local resp = table.concat(response_body)
ngx.log(ngx.DEBUG, "response body: ", resp)
if code ~= 200 then
return nil, resp
end
local decoded, err = cjson.decode(resp)
if err then
ngx.log(ngx.ERR, "failed to decode response body: ", err)
return nil, err
end
if not decoded.expires_in then
return nil, decoded.error or resp
end
if decoded.expires_in <= 0 then
return nil, EXPIRES_ERR
end
decoded.expires_at = decoded.expires_in os.time()
return decoded
end
function TokenAuthHandler:new()
TokenAuthHandler.super.new(self, "token-auth")
end
--- 实现 access 方法
function TokenAuthHandler:access(conf)
TokenAuthHandler.super.access(self)
local token, err = extract_token(ngx.req)
if err then
ngx.log(ngx.ERR, "failed to extract token: ", err)
return kong.response.exit(500, { message = err })
end
ngx.log(ngx.DEBUG, "extracted token: ", token)
local ttype = type(token)
if ttype ~= "string" then
if ttype == "nil" then
return kong.response.exit(401, { message = "Missing token"})
end
if ttype == "table" then
return kong.response.exit(401, { message = "Multiple tokens"})
end
return kong.response.exit(401, { message = "Unrecognized token" })
end
local info
info, err = query_and_validate_token(token, conf)
if err then
ngx.log(ngx.ERR, "failed to validate token: ", err)
if EXPIRES_ERR == err then
return kong.response.exit(401, { message = EXPIRES_ERR })
end
return kong.response.exit(500,{ message = EXPIRES_ERR })
end
if info.expires_at < os.time() then
return kong.response.exit(401, { message = EXPIRES_ERR })
end
ngx.log(ngx.DEBUG, "token will expire in ", info.expires_at - os.time(), " seconds")
end
return TokenAuthHandler
token-auth 插件实现了 new() 和 access() 两个方法,只在 access 阶段发挥作用。在 access() 方法中,首先会提取 JWT 头部信息,检查 token 是否存在以及格式是否正确等,随后请求认证服务器验证 token 的合法性。
加载插件
插件开发完成后,首先要在插件目录中新建 token-auth-1.2.1-0.rockspec 文件,填写新开发的插件:
代码语言:javascript复制package = "token-auth"
version = "1.2.1-0"
supported_platforms = {"linux", "macosx"}
local pluginName = "token-auth"
build = {
type = "builtin",
modules = {
["kong.plugins.token-auth.handler"] = "kong/plugins/token-auth/handler.lua",
["kong.plugins.token-auth.schema"] = "kong/plugins/token-auth/schema.lua",
}
}
然后在 kong.conf 配置文件中添加新开发的插件:
代码语言:javascript复制$ vim /etc/kong/kong.conf
# 去掉开头的注释并修改如下
plugins = bundled, token-auth
bundled 属性是指官方提供的插件合集,默认开启。这里,我们增加了自定义的 token-auth 插件。验证一下,自定义的插件是否成功加载:
代码语言:javascript复制$ curl http://127.0.0.1:8001/plugins/enabled
{"enabled_plugins":["correlation-id","pre-function","cors","token-auth","ldap-auth","loggly","hmac-auth","zipkin","request-size-limiting","azure-functions","request-transformer","oauth2","response-transformer","ip-restriction","statsd","jwt","proxy-cache","basic-auth","key-auth","http-log","datadog","tcp-log","post-function","prometheus","acl","kubernetes-sidecar-injector","syslog","file-log","udp-log","response-ratelimiting","aws-lambda","bot-detection","rate-limiting","request-termination"]}%
启用插件
在 Service 上启用 token-auth 插件,同时需要指定 config.auth_server_url 的属性:
代码语言:javascript复制$ curl -i -XPOST localhost:8001/services/aoho-blog/plugins
--data 'name=token-auth'
--data 'config.auth_server_url=<URL of verification API>'
如果插件有自己的数据库表,或者对数据库表或表中数据有要求,在插件目录中创建 migrations 目录。根据使用的是 Postgres 还是 Cassandra,创建 migrations/postgres.lua 或者 migrations/cassandra.lua。
如果插件有自己的数据库表,还需要在插件目录中创建 daos.lua,返回数据库表定义,如果没有单独的数据库表,不需要创建这个文件。
这里不做过多演示,读者可以结合笔者之前的 chat:统一认证与授权在微服务架构中的设计与实战,构建认证授权服务器,自行尝试一下。
小结
网关是微服务架构中不可或缺的基础服务,本文介绍了如何使用 Kong 构建微服务网关。相比于其他网关组件,Kong 在易用性和性能方面表现优异,是一款现代的云原生网关。随后介绍了 Kong 的部分插件使用。Kong 官方和社区提供了丰富的 API 网关插件,配置即可使用。最后,笔者在文中实现了一个自定义的 token-auth 的插件,Kong 开放的插件机制,使得开发者可以灵活地实现特殊的业务需求。