云原生架构下的 API 网关实践: Kong (三)

4,213 阅读5分钟

在前面的文章介绍了 Kong 的相关实践,文章链接,本文将会介绍 Kong 的利器:插件以及自定义插件。

Kong 几种常用插件的应用

请求到达 Kong,在转发给服务端应用之前,我们可以应用 Kong 自带的插件对请求进行处理,如合法认证、限流控制、黑白名单校验和日志采集等等。同时,我们也可以按照 Kong 的教程文档,定制开发属于自己的插件。本小节将会选择其中的两个插件示例应用,其余的插件应用,可以参见:docs.konghq.com/hub/。

JWT 认证插件

JWT 是目前最流行的跨域身份验证解决方案。作为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的。

关于为什么使用 JWT,不在本小节详细论述,具体可见 统一认证与授权在微服务架构中的设计与实战。Kong 提供了 JWT 认证插件,用以验证包含 HS256 或 RS256 签名的 JWT 的请求(如RFC 7519中所述)。每个消费者都将拥有 JWT 凭证(公钥和密钥),这些凭证必须用于签署其 JWT。JWT 令牌可以通过请求字符串、cookie 或者认证头部传递。Kong 将会验证令牌的签名,通过则转发,否则直接丢弃请求。

我们在前面小节配置的路由基础上,增加 JWT 认证插件。

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 凭证,执行如下的调用:

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 插件:

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 插件,将插件绑定到路由上(这里可以绑定为全局的插件)。

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,安装较为简单:

 $ brew tap kong/kong
 $ brew install kong

其次安装 Postgres,并下载 kong.conf.default 配置文件(参见 raw.githubusercontent.com/Kong/kong/m…

 $ sudo mkdir -p /etc/kong
 $ sudo cp kong.conf.default /etc/kong/kong.conf

执行 migration:

kong migrations bootstrap -c /etc/kong/kong.conf

随后即可启动 Kong:

kong start -c /etc/kong/kong.conf

启动之后,通过 8001 管理端口验证是否成功。

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 所需的配置定义如下:

return {
  no_consumer = true,
  fields = {
    auth_server_url = {type = "url", required = true},
  }
}

从 schema.lua 可以看到,启用 token-auth 插件时,需要检查 auth_server_url 字段为 URL 类型,且不能为空。

插件功能实现:handler.lua

handler.lua 实现了插件认证功能,这个插件中定义的方法,会在处理请求和响应的时候被调用。

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 文件,填写新开发的插件:

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 配置文件中添加新开发的插件:

$ vim /etc/kong/kong.conf

# 去掉开头的注释并修改如下
plugins = bundled, token-auth

bundled 属性是指官方提供的插件合集,默认开启。这里,我们增加了自定义的 token-auth 插件。验证一下,自定义的插件是否成功加载:

$ 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 的属性:

$ 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 开放的插件机制,使得开发者可以灵活地实现特殊的业务需求。

推荐阅读

云原生架构下的 API 网关实践

订阅最新文章,欢迎关注我的公众号

微信公众号