API網關自定義插件

需求背景

在完成API網關的一系列部署和配置以後,下一步在系統上須要對應用程序疊加自定義的插件,主要用於認證與鑑權邏輯。Kong社區版自己集成了衆多的插件,其中也包括認證相關的oauth二、jwt等插件,但使用的時候須要和kong內部的consumer結合,也就意味着應用系統設計上須要和kong的數據庫進行交互。nginx

對應用系統而言,早期網關功能由nginx來實現,其認證鑑權的業務邏輯由應用系統自身來實現,微服務改造以後,但願網關層可以承擔起認證與鑑權的角色,鑑於此,咱們決定採用自定義插件的形式來實現微服務的認證與鑑權。redis

PDK介紹

1、插件的目錄規範數據庫

在插件目錄下必須至少存在handler.lua和schema.lua兩個文件。
其餘可選的文件有api.lua,daos.lua、migrations/.lua等
其中handler.lua用於實現業務路徑,schema.lua用於用戶自定義配置
api.lua用於定義admin api,若是須要使用kong的內置數據庫對象,還應當存在daos.lua文件,migrations/
.lua用於數據遷移相關json

2、插件的加載後端

在主配置文件中須要聲明加載自定義插件的名稱,如自定義插件不存在默認位置則須要配置路徑
插件默認路徑爲:/usr/local/share/lua/5.1/kong/plugins/api

參考:https://docs.konghq.com/enterprise/2.3.x/plugin-development/file-structure/跨域

網關上下文理解

對於HTTP/HTTPS 請求,涉及的請求生命週期上下文及函數
:init_worker()函數對應init_worker階段,即每次Nginx worker進程啓動時執行
:certificate()函數對應ssl_certificate階段,即在SSL握手的SSL證書服務階段執行
:rewrite()函數對應rewrite階段,即在每一個請求的重寫階段執行
:access()函數對應access階段,即在每一個請求被代理到上游服務以前執行
:header_filter()函數對應header_filter階段,當已從上游服務接收到全部響應頭字節時執行
:body_filter()函數對應body_filter階段對從上游服務接收到的響應主體的每一個塊執行
:log()函數對應log階段最後一個響應字節已發送到客戶端時執行 併發

鑑權和認證的業務邏輯須要放在access階段app

參考:https://docs.konghq.com/enterprise/2.3.x/plugin-development/custom-logic/ide

插件開發

1、handler.lua

--引用包
local redis = require "resty.redis"
local cjson = require "cjson.safe"

local plugin = {
  PRIORITY = 1000,
  VERSION = "0.1",
}

-- 讀取redis值函數
local function redis_get(conf,key)
  -- 鏈接redis
  local red = redis:new()
  red:set_timeout(conf.redis_conn_timeout)
  local conn_ok, conn_err = red:connect(conf.redis_ip, conf.redis_port)
  red:auth(conf.redis_password)
  if not conn_ok then
    kong.response.exit(500,{message = "redis鏈接失敗: "..conn_err})
  end
  --調用hget獲取值
  local get_ok, get_err = red:get(key)
  --若是hget未獲取到
  if not get_ok then
    kong.response.exit(500,{message = "redis獲取"..key.."失敗: "..get_err})
  end
  --鏈接池默認100個,默認超時時間60s
  local keep_ok, keep_err = red:set_keepalive(conf.redis_pool_timeout, conf.redis_pool_size)
  if not keep_ok then
    kong.response.exit(500,{message = "redis鏈接池設置失敗: "..keep_err})
  end
  return get_ok
end

--base64解碼App-Authentication,返回解碼後的json
local function decode_appauth(app_auth)
  --去掉Basic字符
  local app_64 = string.gsub(app_auth,"Basic ",'')
  --使用base64解密
  local app = ngx.decode_base64(app_64)
  if not app then
    kong.response.exit(401,{message = "App-Authentication 解密失敗"})
  end
  --轉換爲json結構
  local json_ok,json_err = cjson.decode(app)
  if not json_ok then
    kong.response.exit(401,{message = "App-Authentication json解析失敗: "..json_err})
  end
  return json_ok
end

--生成網關用戶gateway_user頭
local function generate_gateway_user(conf,token)
  if token == nil then
    return nil
  end
  --生成當前用戶信息數據的redis key名
  local current_user_key =  "auth_to_user_info:"..string.gsub(token,"Bearer ",'')
  local current_user_value = redis_get(conf,current_user_key)
  local current_user_json = cjson.decode(current_user_value)
  --從用戶信息數據的redis中解析出來,並拼湊gateway_user
  local gateway_user
  if current_user_value ~= ngx.null and current_user_json then
    gateway_user=cjson.encode({
      ["platformId"] = current_user_json.platformId,
      ["platformVersionId"] = current_user_json.platformVersionId,
      ["projectId"] = current_user_json.projectId,
      ["subProjectId"] = current_user_json.subProjectId,
      ["unitId"] = current_user_json.unitId,
      ["organizationId"] = current_user_json.organizationId,
      ["userId"] = current_user_json.userId,
      ["username"] = current_user_json.username,
    })
  end
  return gateway_user
end

function plugin:access(plugin_conf)
  --請求爲OPTIONS不獲取請求頭,直接跳過
  if kong.request.get_method() == "OPTIONS"
  then
    return
  end
  --獲取請求頭
  local app_auth = kong.request.get_header("App-Authentication")
  local token = kong.request.get_header("Authorization")
  if app_auth == nil   then
    kong.response.exit(401,{message = "請求頭缺失"})
  end
  --解析App-Authentication請求頭,獲取平臺ID等內容的json
  local app_json = decode_appauth(app_auth)
  --網關上下文,從App-Authentication中解析出來,並拼湊gateway_context
  local gateway_context=cjson.encode({
    ["platformId"] = app_json.platformId,
    ["platformVersionId"] = app_json.platformVersionId,
    ["projectId"] = app_json.projectId,
    ["subProjectId"] = app_json.subProjectId,
  })

  --生成用戶信息數據gateway_user
  local gateway_user = generate_gateway_user(plugin_conf,token)
  ngx.req.set_header("Gateway-Context",gateway_context)
  ngx.req.set_header("Gateway-User",gateway_user)

end

return plugin

2、schema.lua

local typedefs = require "kong.db.schema.typedefs"

return {
  name = "gateway",
  fields = {
    { protocols = typedefs.protocols_http },
    { config = {
        type = "record",
        fields = {
          { redis_ip = typedefs.ip({ required = true }) },
          { redis_port = typedefs.port({ required = true }) },
          { redis_password = { type = "string", default = "Please input redis password" }, },
          { redis_conn_timeout = typedefs.timeout({ required = true ,default = 1000,}) },
          { redis_pool_timeout = typedefs.timeout({ required = true ,default = 60000,}) },
          { redis_pool_size = { type = "number", default = 100, } },
          { jwt_signature = { type = "string", default = "Please input jwt signature", }, },
          { sso_url = typedefs.url({ required = true }) },
    }, }, },
  },
}

3、說明

初版的自定義網關插件不涉及認證與鑑權業務邏輯,插件工做邏輯以下
一、判斷若是請求方法爲OPTIONS則直接轉發,對應跨域相關的請求
二、獲取請求頭中的App-Authentication、Authorization字段
三、若是App-Authentication請求頭不存在,返回狀態碼401
四、根據獲取到的App-Authentication請求頭信息,進行base64解析,最終拼接成gateway_context網關上下文
五、根據獲取到的Authorization請求頭信息,從redis中獲取數據,最終拼接成gateway_user網關上下文
六、轉發到後端的微服務,並附帶gateway_context、gateway_user請求頭

最終認證與鑑權版本自定義網關插件的業務邏輯較爲複雜,且涉及具體業務邏輯,所以不進行展現和說明。

插件部署

因爲kong採用k8s方式部署,所以配置文件咱們採用configmap外掛形式實現,因爲插件目前還沒有進入穩定階段,需求變動相對頻繁,因此暫定插件目錄採起PVC外掛的形式實現,插件代碼更新不須要進行鏡像編譯。同時考慮到代碼bug,插件大併發下的性能問題,咱們插件分爲鑑權和不鑑權兩個版本,配置在微服務的route裏面,如出現性能或bug問題,能夠在konga面板上快速禁用和啓用,避免形成大面積的故障。

一、系統配置文件

grep 'plugins' kong.conf  |grep -v '#' 
plugins = bundled,gateway,gateway-auth
grep 'lua_package_path' kong.conf  |grep -v '#'
lua_package_path = ./?.lua;./?/init.lua;/mnt/mfs/?.lua;;

二、將配置文件導入爲configmap

kubectl create cm kong-conf -n kong --from-file=kong.conf

三、工做負載聲明文件中引用插件目錄和configmap

volumes:
        - name: vol-localtime
          hostPath:
            path: /etc/localtime
            type: ''
        - name: mfsdata
          persistentVolumeClaim:
            claimName: mfsdata-kong
        - name: kong-conf
          configMap:
            name: kong-conf
            items:
              - key: kong.conf
                path: kong.conf
 volumeMounts:
          - name: vol-localtime
            readOnly: true
            mountPath: /etc/localtime
          - name: mfsdata
            mountPath: /mnt/mfs
          - name: kong-conf
            mountPath: /etc/kong/kong.conf
            subPath: kong.conf

測試與驗證

API網關自定義插件

API網關自定義插件
API網關自定義插件
API網關自定義插件
API網關自定義插件

相關文章
相關標籤/搜索