需求背景
在完成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
测试与验证