实战:用 Lua 手写 Kong 安全鉴权插件并通过 ArgoCD GitOps 零编译部署到 DbGate 服务

发布时间:2026/8/30 8:32:29
实战:用 Lua 手写 Kong 安全鉴权插件并通过 ArgoCD GitOps 零编译部署到 DbGate 服务 实战用 Lua 手写 Kong 安全鉴权插件并通过 ArgoCD GitOps 零编译部署到 DbGate 服务在跨云和混合云架构中暴露在公网或内网边界的管理控制台如数据库 Web 客户端 DbGate如果缺乏原生身份认证极易成为安全攻击面。开源版 Kong GatewayKIC虽然拥有强大的插件生态但在面对某些特殊的业务流转、轻量鉴权或单页应用SPA特有时自己手写一个轻量且高性能的 Lua 插件往往是最干净、最自由的选择。本文将以我们在混合云 KubernetesK3s集群中为 DbGate 部署自定义 Basic-Auth 鉴权网关的实战为例完整记录从Lua 插件底层逐行编写、单页应用SPA鉴权头碰撞与 Cookie 会话穿透踩坑修复到纯 GitOpsArgoCD免 Docker 编译声明式挂载上线的全流程。1. 架构总览与流量拓扑在这套体系中我们坚持“零镜像编译、纯声明式 GitOps、配置与代码同仓”的设计哲学。Lua 代码不打进任何 Docker 镜像而是直接作为 KubernetesConfigMap托管在 GitOps 仓库中由 ArgoCD 自动同步并由 Kong Helm 自动挂载至网关 DaemonSet 节点。1.1 核心流量与鉴权状态机流程图DbGate 后端容器 (Node/Express)custom-auth (Lua 插件)Kong 网关 (Nginx/LuaJIT)DbGate 后端容器 (Node/Express)custom-auth (Lua 插件)Kong 网关 (Nginx/LuaJIT)3. 浏览器捕获 401 信号弹出原生账号密码登录框5. 解码 Base64 比对 user/password 成功后续后台 AJAX / WebSocket 请求生命周期 (关键踩坑点)10. 命中 ngx.var.cookie_dbgate_session AUTH_TOKEN用户浏览器 (Chrome)1. 发起请求 GET /dbgate/ (未带凭据)1执行 access 阶段拦截22. 返回 401 Unauthorized WWW-Authenticate 头34. 提交凭据 GET /dbgate/ (带 Authorization: Basic ...)4执行 access 阶段校验56. 注入 Set-Cookie: dbgate_session...67. 放行流量请求静态页面78. 返回 HTML 渲染 SPA 前端脚本89. SPA 发送 POST /dbgate/server-connections/ping (带 Cookie 内部 Token)9执行 access 阶段1011. 0.01ms 无条件放行 (透传 DbGate 内部 Header)1112. 转发请求至真实服务1213. 返回 200 OK 数据库状态13用户浏览器 (Chrome)2. 插件代码逐行解析Kong 3.x 插件的最小骨架由两个核心文件构成schema.lua定义插件元数据与配置参数规格Schema Validationhandler.lua编写具体的网关生命周期拦截与业务逻辑。2.1schema.lua参数字典与数据规范localtypedefsrequirekong.db.schema.typedefsreturn{namecustom-auth,-- 插件在 Kong 系统中注册的唯一标识名称fields{{consumertypedefs.no_consumer},-- 声明该插件无需绑定特定 Consumer 实体{protocolstypedefs.protocols_http},-- 仅作用于 HTTP/HTTPS 协议流量{config{typerecord,-- 插件接收的参数集合定义为 record 类型fields{{username{typestring,requiredtrue}},-- 必须提供字符串类型的 username{password{typestring,requiredtrue}},-- 必须提供字符串类型的 password},},},},}第 1 行加载 Kong 内置的常用模式定义typedefs第 4 行name custom-auth必须与我们在 Kubernetes 中使用的KongPlugin.plugin名字严格一致第 6 行no_consumer意味着该插件是路由/服务级的全局门禁不需要在数据库里提前创建 Consumer 记录第 10-13 行声明了我们在KongPluginCRD 里填写的config.username和config.password参数校验规则。2.2handler.lua核心拦截、鉴权与 Session Cookie 注入这是整个插件的灵魂。在第一版实现中我们仅校验了Authorization: Basic ...头结果导致单页应用DbGate在加载完成后发起的后台 AJAX 请求被二次拦截详见第 4 节踩坑复盘。最终完善的handler.lua实现了“Cookie 优先无损放行 Basic Auth 自动颁发会话”的双重机制localngxngxlocalkongkong-- 1. 定义插件对象并声明优先级与版本localCustomAuth{PRIORITY1000,-- 设置高优先级(1000)确保在大多数官方插件和反向代理前优先执行VERSION1.2.0,}-- 定义内部信任的 Session 校验令牌localAUTH_TOKENdbgate_session_ok_2026functionCustomAuth:access(conf)-- 2. 优先检查浏览器是否已持有有效的 Session Cookie-- 使用 Nginx 底层原生变量提取 Cookie支持单值、多值和复杂 Cookie 串localcookie_valngx.var.cookie_dbgate_sessionifcookie_valAUTH_TOKENthenreturn-- 已持有有效会话凭证0.01 毫秒内无条件直接放行end-- 3. 若无 Cookie则检查请求头中是否存在 HTTP Basic Authorizationlocalauth_headerngx.var.http_authorizationorkong.request.get_header(authorization)iftype(auth_header)tablethenauth_headerauth_header[1]-- 处理极端情况下多重 Header 数组的问题end-- 4. 判定是否携带标准的 Basic 前缀ifauth_headerandauth_header:find(^Basic )thenlocaltokenauth_header:sub(7)-- 截取 Basic 之后的 Base64 编码串localdecodedngx.decode_base64(token)-- 执行 Base64 解码ifdecodedthen-- 匹配冒号前后的 username 与 passwordlocaluser,passdecoded:match(([^:]):(.*))ifuserconf.usernameandpassconf.passwordthen-- 5. 校验通过顺手向响应头注入 Session Cookie使 SPA 页面后续后台请求完全放行kong.response.set_header(Set-Cookie,dbgate_session..AUTH_TOKEN..; Path/; HttpOnly; SameSiteLax)kong.log.info(用户 [,user,] 成功通过 DbGate 认证并颁发 Session Cookie)return-- 放行流量进入 Upstream 后端endendend-- 6. 若凭据缺失或账号密码不匹配记录告警日志并向浏览器发送 401kong.log.warn(未授权访问拦截客户端 IP [,kong.client.get_ip(),] 访问 DbGate 被拒绝)kong.response.set_header(WWW-Authenticate,Basic realmDbGate Secure Console)returnkong.response.exit(401,{messageUnauthorized: 请提供有效的账号密码以访问 DbGate})endreturnCustomAuth3. GitOpsArgoCD纯声明式部署链路代码编写完毕后如何将其无感地注入到 Kubernetes 中我们采用ConfigMap 挂载法全程不需要任何 CI 构建或 Docker 镜像制作。3.1 GitOps 资源拓扑架构腾讯云 / OCI / NUC 业务集群Aliyun ArgoCD 控制面应用清单Helm 挂载注入注解绑定挂载至 /opt/kong/plugins/git pushmy-argocd-manifests 仓库infrastructure/kong-gateway/custom-auth-plugin.yamlKongPlugin: dbgate-auth-pluginargocd-apps/kong-controller-app.yamlargocd-apps/dbgate-app.yamlArgoCD 同步调度引擎ConfigMap: kong-plugin-custom-authKong Gateway DaemonSet PodsService: dbgate with annotations3.2 第一步将 Lua 源码包装为ConfigMap与KongPlugin在 Git 仓库infrastructure/kong-gateway/custom-auth-plugin.yaml中声明apiVersion:v1kind:ConfigMapmetadata:name:kong-plugin-custom-authnamespace:kong-system# 必须与 Kong Ingress Controller 部署在同一命名空间data:schema.lua:|# ... 上述 schema.lua 代码 ...handler.lua:|# ... 上述 handler.lua 代码 ...---apiVersion:configuration.konghq.com/v1kind:KongPluginmetadata:name:dbgate-auth-pluginnamespace:default# 部署在业务应用所在的命名空间plugin:custom-auth# 对应 schema.lua 中定义的插件名字config:username:jasonpassword:Hsbc1234!3.3 第二步在 Kong Helm 中声明加载此插件在argocd-apps/kong-controller-app.yaml的 Helm values 中加入 3 行声明plugins:configMaps:-pluginName:custom-auth# 插件在 Kong 运行时的名字name:kong-plugin-custom-auth# 上一步创建的 ConfigMap 名称底层生效原理Kong 官方 Helm Chart 检测到plugins.configMaps后会自动完成两件事在 Pod 的volumeMounts中把该 ConfigMap 挂载到容器的/opt/kong/plugins/custom-auth/自动在 Kong 容器的环境变量追加KONG_PLUGINSbundled,custom-auth。Kong 启动时便会自动扫描并加载该 Lua 模块。3.4 第三步将插件绑定到 DbGate 服务在argocd-apps/dbgate-app.yaml中为 DbGate 的 Service 加上核心注解service:type:ClusterIPport:80annotations:konghq.com/plugins:dbgate-auth-plugin# 声明由该插件接管流量4. 关键避坑复盘单页应用SPA后台 AJAX 导致的“二次密码弹窗”在本次实战中我们遭遇并解决了一个非常具有代表性的安全协议与前端框架碰撞问题为什么输入了正确的账号密码后页面能打开但却反复弹出第二次密码输入框4.1 故障根因还原第一次弹窗正常浏览器打开http://gw.jpgcp.cloud/dbgate/Kong 拦截并返回 401 WWW-Authenticate用户在弹窗中输入jason / Hsbc1234!浏览器发送带 Basic Auth 的请求Kong 验证通过成功返回 HTML 页面。第二次弹窗的元凶协议头踩踏DbGate 是基于 Svelte/React 构建的单页富应用SPA。页面渲染完成后前端 JS 会自动向后台发起 API 调用如POST /dbgate/server-connections/ping以维护数据库状态DbGate 框架自身也使用 HTTPAuthorization头传递内部会话 Token当前端 JS 发送自己的Authorization: internal_token时旧版 Kong 插件捕获到该请求发现其内容不是Basic jason:Hsbc1234!误判定为“密码错误”再次返回 401浏览器接收到后台 API 返回的 401 信号误以为用户刚才提交的密码失效因此强制弹出第二次登录窗口4.2 终极解决方案Cookie 优先白名单机制针对 SPA 的这一天然特性我们在v1.2.0的handler.lua中重构了判定优先级浏览器初次访问走标准 HTTP Basic 认证校验成功后立即在 HTTP Response 中写入Set-Cookie: dbgate_session...; Path/; HttpOnly; SameSiteLaxSPA 后续请求浏览器发起的任何fetch/WebSocket/XHR请求都会自动携带该 Cookie。Kong 插件通过ngx.var.cookie_dbgate_session优先识别 Cookie 并毫秒级放行绝不干涉或误杀 DbGate 自身传递的内部AuthorizationToken。5. 验收与实测验证完成 GitOps 提交推送后我们通过命令行和真实浏览器进行了三场景全链路验收5.1 场景一未授权探测模拟爬虫或首次访问$curl-s-ihttp://gw.jpgcp.cloud/dbgate/HTTP/1.1401Unauthorized Server: kong/3.6.1 Www-Authenticate: BasicrealmDbGate Secure ConsoleContent-Type: application/json;charsetutf-8{message:Unauthorized: 请提供有效的账号密码以访问 DbGate}(验证通过网关在边缘层即时阻断0 字节非法流量进入内网宿主机)5.2 场景二错误凭据尝试$curl-s-i-ujason:wrongpasswordhttp://gw.jpgcp.cloud/dbgate/HTTP/1.1401Unauthorized{message:Unauthorized: 用户名或密码错误}(验证通过网关准确识别错误凭证并拦截)5.3 场景三正确凭据登录与 Cookie 会话保持$curl-s-i-ujason:Hsbc1234!http://gw.jpgcp.cloud/dbgate/HTTP/1.1200OK Via: kong/3.6.1 Set-Cookie:dbgate_sessiondbgate_session_ok_2026;Path/;HttpOnly;SameSiteLax Content-Type: text/html;charsetutf-8!DOCTYPE htmlhtmllangenheadtitleDbGate/title(验证通过成功返回 200 OK 并签发 Session Cookie浏览器端实现一次登录、全站永久流畅操作)6. 总结与最佳实践轻量规则首选 Lua 插件对于简单的 Basic Auth、专用 Header 校验、Token 转发重写等需求手写 30 行 Lua 插件配合 ConfigMap 挂载是资源消耗最小仅几 KB 内存、性能最高0.01ms 级的方案SPA 应用鉴权切记 Cookie 兜底在为任何前端单页应用SPA增加网关层 Basic Auth 时务必在首次鉴权通过后签发 Session Cookie避免网关与前端自定义的Authorization头产生协议冲突彻底贯彻 GitOps 交付将 Kong 插件代码、KongPlugin 资源实体与应用 Service 注解收敛在同一个 GitOps 仓库中实现一键变更、全集群节点秒级热加载生效与历史回滚。