46

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

 5 years ago
source link: https://www.tuicool.com/articles/vIzIzyz
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在前面的文章介绍了 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 认证插件。

curl -X POST http://localhost:8001/routes/e33d6aeb-4f35-4219-86c2-a41e879eda36/plugins \
--data "name=jwt"

可以看到,在插件列表增加了相应的记录。

nYfiyyf.jpg!web

在增加了 JWT 插件之后,就没法直接访问 /api/blog 接口了,接口返回: "message": "Unauthorized" 。提示客户端要访问需要提供 JWT 的认证信息。因此,我们需要创建用户:

curl -i -X POST \
--url http://localhost:8001/consumers/  \
--data "username=aoho"

如上创建了一个名为 aoho 的用户。

A7jIfem.jpg!web

创建好用户之后,需要获取用户 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。

bee6Jvf.jpg!web

将生成的 Token,配置到请求的认证头部,再次执行请求:

FJbqemJ.jpg!web

可以看到,我们能够正常请求相应的 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 服务上。

r6BrAvB.jpg!web

通过访问 /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%,生产环境谨慎使用,采样率对系统吞吐量有影响。

vaIf2aR.jpg!web

可以看到,Zipkin 插件已经应用到指定的路由上。下面我们将会执行请求 /api/blog 接口,打开 http://localhost:9411 界面如下:

rEryAzB.jpg!web

Zipkin 已经将请求记录,我们可以点开查看详细的链路详情:

zIRVjeQ.jpg!web

从链路调用可以知道,请求到达 Kong 之后,经历了哪些服务和 Span,每个 Span 所花费的时间等等信息。

自定义插件的实践

官方虽然提供了很多插件,但是我们在实际的业务场景中还会有业务的需求,定制插件能够帮助我们更好地管理 API Gateway。Kong 提供了插件开发包和示例,自定义插件只需要按照提供的步骤即可。

Kong 安装

在上面小节,笔者介绍了通过镜像的方式安装 Kong,本部分为了方便编写自定义插件,我们使用本地安装的 Kong,笔者的环境是 macOS,安装较为简单:

$ brew tap kong/kong
$ brew install kong

其次安装 Postgres,并下载 kong.conf.default 配置文件(参见 https://raw.githubusercontent.com/Kong/kong/master/kong.conf.default),执行如下的命令:

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK