雲原生架構下的 API 網關實踐:Kong (三)

在前面的文章介紹了 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"

可以看到,在插件列表增加了相應的記錄。

在增加了 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 配置文件(參見 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 開放的插件機制,使得開發者可以靈活地實現特殊的業務需求。

推薦閱讀

雲原生架構下的 API 網關實踐

訂閱最新文章,歡迎關注我的公衆號

微信公衆號

給個[在看],是最大的支持!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章