API网关自定义插件

需求背景

在完成API网关的一系列部署和配置之后,下一步在系统上需要对应用程序叠加自定义的插件,主要用于认证与鉴权逻辑。Kong社区版本身集成了众多的插件,其中也包括认证相关的oauth2、jwt等插件,但使用的时候需要和kong内部的consumer结合,也就意味着应用系统设计上需要和kong的数据库进行交互。

对应用系统而言,早期网关功能由nginx来实现,其认证鉴权的业务逻辑由应用系统自身来实现,微服务改造之后,希望网关层能够承担起认证与鉴权的角色,鉴于此,我们决定采用自定义插件的形式来实现微服务的认证与鉴权。

PDK介绍

一、插件的目录规范

在插件目录下必须至少存在handler.lua和schema.lua两个文件。
其他可选的文件有api.lua,daos.lua、migrations/.lua等
其中handler.lua用于实现业务路径,schema.lua用于用户自定义配置
api.lua用于定义admin api,如果需要使用kong的内置数据库对象,还应当存在daos.lua文件,migrations/
.lua用于数据迁移相关

二、插件的加载

在主配置文件中需要声明加载自定义插件的名称,如自定义插件不存在默认位置则需要配置路径
插件默认路径为:/usr/local/share/lua/5.1/kong/plugins/

参考: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阶段

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

插件开发

一、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

二、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 }) },
    }, }, },
  },
}

三、说明

第一版的自定义网关插件不涉及认证与鉴权业务逻辑,插件工作逻辑如下
1、判断如果请求方法为OPTIONS则直接转发,对应跨域相关的请求
2、获取请求头中的App-Authentication、Authorization字段
3、如果App-Authentication请求头不存在,返回状态码401
4、根据获取到的App-Authentication请求头信息,进行base64解析,最终拼接成gateway_context网关上下文
5、根据获取到的Authorization请求头信息,从redis中获取数据,最终拼接成gateway_user网关上下文
6、转发到后端的微服务,并附带gateway_context、gateway_user请求头

最终认证与鉴权版本自定义网关插件的业务逻辑较为复杂,且涉及具体业务逻辑,因此不进行展示和说明。

插件部署

由于kong采用k8s方式部署,因此配置文件我们采用configmap外挂形式实现,由于插件目前尚未进入稳定阶段,需求变更相对频繁,所以暂定插件目录采取PVC外挂的形式实现,插件代码更新不需要进行镜像编译。同时考虑到代码bug,插件大并发下的性能问题,我们插件分为鉴权和不鉴权两个版本,配置在微服务的route里面,如出现性能或bug问题,可以在konga面板上快速禁用和启用,避免造成大面积的故障。

1、系统配置文件

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;;

2、将配置文件导入为configmap

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

3、工作负载声明文件中引用插件目录和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网关自定义插件

上一篇:说实话,去一家小公司从 0 到 1 搭建后端架构,真难~


下一篇:php实战kong做微服务架构一(kong简介与安装)