ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于 Kong Gateway + Logto + 自定义 Lua 插件打造零信任 GitHub SSO 统一身份网关

基于 Kong Gateway + Logto + 自定义 Lua 插件打造零信任 GitHub SSO 统一身份网关 1. 为什么要在网关层做 GitHub SSO 零信任鉴权Kong Gateway 是 OpenResty 生态里最成熟的 API 网关之一Logto 是开源的 OIDC 身份平台两者组合起来可以在网关层完成 GitHub SSO 的零信任鉴权。这套方案适合谁适合在 Kubernetes 里跑了一堆内部工具数据库管理台、大模型网关控制台、监控面板的团队尤其是那些不想给每个服务单独写登录逻辑、又不想买 Kong Enterprise 的开发者。我试过的场景是这样的集群里跑着 DbGate 和 LiteLLM 两个控制台前者是数据库管理界面后者是大模型统一网关。每个服务都有自己的登录方式有的用 Basic Auth有的干脆裸奔在内网。问题在于一旦通过 Ingress 暴露到公网这些控制台就成了攻击面。更麻烦的是LiteLLM 的推理 API 需要给自动化脚本和 Coding Agent 直接调用如果整个服务都挂上 SSO 拦截脚本调用会收到 302 重定向到 HTML 登录页链路直接断掉。所以核心诉求有两个第一Web 控制台必须走 GitHub SSO 登录未登录请求在网关层就被拦截第二推理 API 路径要能绕过 SSO由服务自身的 Bearer Token 校验。Kong 社区版没有 forward-auth 插件openid-connect 也是企业版专属所以需要自己写一个 Lua 插件来实现 Forward-Auth 协议。Forward-Auth 的机制不复杂Kong 收到外部请求后不直接转发给业务 Pod而是先提取 Cookie向集群内的 oauth2-proxy 发起一个子请求。oauth2-proxy 返回 200 或 202 表示已登录Kong 就把身份头注入请求转发给后端返回 401 或 403 表示未登录Kong 根据请求类型决定是返回 401 JSON 还是 302 重定向到登录页。整个过程中业务服务完全不知道 SSO 的存在零侵入。2. TaoToken 前置准备API Key 与接入信息在开始配置之前需要先准备好模型服务的接入信息。TaoToken 提供了统一的 API 入口兼容 OpenAI 接口格式适合作为 LiteLLM 上游的模型提供方。你可以先到 TaoToken 控制台 创建一个 API Key后续在 LiteLLM 的配置里会用到。创建 Key 的路径是登录后进入控制台找到 API Keys 页面点击创建。生成的 Key 格式类似sk-xxxxxxxx复制保存好后面配置 LiteLLM 的 model_list 时需要填入。如果你还没注册可以先从 官网 了解整体能力。TaoToken 的 API 基础地址是https://taotoken.net/api这个地址在 LiteLLM 的 config.yaml 里作为api_base使用。注意不要加 UTM 参数直接写裸地址即可。如果你需要查看完整的接入文档可以访问 接入文档里面有各语言 SDK 的调用示例。对于需要长期跑 Coding Agent 的场景比如 Claude Code 或 OpenCode 这类工具建议关注 Coding Plan它针对高频编码调用做了优化。如果你只是想先验证模型对话是否通可以直接在 模型对话 页面测试。准备好 Key 之后我们进入 Kong 和 Logto 的配置环节。3. 可复制配置Kong 声明式配置 Logto OIDC Lua 插件3.1 Logto 侧创建 OIDC 应用并接入 GitHub先在 Logto 控制台创建一个传统 Web 应用类型选 Traditional Web。创建完成后记录三个关键参数App ID即 client_id、App Secret即 client_secret、OIDC Issuer URL格式为https://xxxx.logto.app/oidc。然后在 Logto 的 Social Connectors 里添加 GitHub 连接器。需要去 GitHub 的 Developer Settings 创建一个 OAuth App回调地址填 Logto 提供的https://xxxx.logto.app/callback/github。拿到 GitHub 的 Client ID 和 Client Secret 后填入 Logto 连接器配置。这里有一个关键设置在 Logto 的 Sign-in Experience 里把signInMode设为SignIn only关闭公开注册。这样即使陌生人用自己的 GitHub 账号点击授权也会被 Logto 拦截提示该应用未开放公开注册。这是零信任的第一道门。3.2 OAuth2-Proxy 部署配置OAuth2-Proxy 作为 OIDC Relying Party负责处理标准的 OIDC 授权码流程、维护 Cookie 状态。以下是 Helm values 的关键片段# oauth2-proxy values.yaml replicaCount: 1 containerPort: 4180 image: repository: quay.io/oauth2-proxy/oauth2-proxy tag: v7.8.1 env: - name: OAUTH2_PROXY_PROVIDER value: oidc - name: OAUTH2_PROXY_OIDC_ISSUER_URL value: https://xxxx.logto.app/oidc - name: OAUTH2_PROXY_CLIENT_ID value: your-logto-app-id - name: OAUTH2_PROXY_CLIENT_SECRET value: your-logto-app-secret - name: OAUTH2_PROXY_COOKIE_SECRET value: 随机生成的32字节base64字符串 - name: OAUTH2_PROXY_HTTP_ADDRESS value: 0.0.0.0:4180 - name: OAUTH2_PROXY_UPSTREAMS value: static://202 - name: OAUTH2_PROXY_USER_ID_CLAIM value: sub - name: OAUTH2_PROXY_OIDC_EMAIL_CLAIM value: sub - name: OAUTH2_PROXY_EMAIL_DOMAINS value: * - name: OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL value: true - name: OAUTH2_PROXY_COOKIE_SECURE value: true - name: OAUTH2_PROXY_COOKIE_HTTPONLY value: true - name: OAUTH2_PROXY_COOKIE_SAMESITE value: lax - name: OAUTH2_PROXY_COOKIE_DOMAINS value: .yourdomain.com - name: OAUTH2_PROXY_COOKIE_NAME value: _oauth2_proxy - name: OAUTH2_PROXY_REVERSE_PROXY value: true - name: OAUTH2_PROXY_SET_XAUTHREQUEST value: true - name: OAUTH2_PROXY_PASS_ACCESS_TOKEN value: true - name: OAUTH2_PROXY_REDIRECT_URL value: https://gw.yourdomain.com/oauth2/callback service: type: ClusterIP port: 4180这里有几个容易踩坑的地方。OAUTH2_PROXY_USER_ID_CLAIM和OAUTH2_PROXY_OIDC_EMAIL_CLAIM都设为sub是因为部分 GitHub 用户没有公开邮箱Logto 下发的 ID Token 里可能不含 email 字段OAuth2-Proxy 默认按 email 做身份标识会直接 500。OAUTH2_PROXY_UPSTREAMS设为static://202表示它只做鉴权不代理实际业务流量返回 202 告诉 Kong 这个人已登录。3.3 Kong Lua 插件骨架这是整套方案的核心。插件需要实现schema.lua和handler.lua两个文件通过 ConfigMap 挂载到 Kong Pod。-- handler.lua local http require resty.http local ngx ngx local kong kong local OAuth2ForwardAuth { PRIORITY 1001, VERSION 1.0.0, } function OAuth2ForwardAuth:access(conf) local req_uri ngx.var.request_uri or kong.request.get_path_with_query() local headers kong.request.get_headers() -- 组装转发给 OAuth2-Proxy 的请求头 local fwd_headers {} for k, v in pairs(headers) do local lk k:lower() if lk ~ host and lk ~ content-length and lk ~ content-type then fwd_headers[k] v end end fwd_headers[X-Forwarded-Uri] req_uri fwd_headers[X-Forwarded-Method] kong.request.get_method() fwd_headers[X-Forwarded-Host] kong.request.get_host() fwd_headers[X-Forwarded-Proto] kong.request.get_scheme() -- 向集群内 OAuth2-Proxy 发起子请求 local httpc http.new() httpc:set_timeout(conf.timeout or 3000) local res, err httpc:request_uri(conf.auth_url, { method GET, headers fwd_headers, keepalive_timeout conf.keepalive_timeout or 60000, keepalive_pool 10, }) if not res then kong.log.err(OAuth2-Proxy connection failed: , err) return kong.response.exit(502, { message Authentication service unreachable }) end -- 校验通过 if res.status 200 or res.status 202 then if res.headers[set-cookie] then local set_cookie res.headers[set-cookie] if type(set_cookie) table then for _, sc in ipairs(set_cookie) do kong.response.add_header(Set-Cookie, sc) end else kong.response.set_header(Set-Cookie, set_cookie) end end for hname, hval in pairs(res.headers) do if hname:lower():find(^x%-auth%-request%-) then kong.service.request.set_header(hname, hval) end end return end -- 未认证拦截 if res.status 401 or res.status 403 then local accept (headers[accept] or ):lower() local x_req (headers[x-requested-with] or ):lower() -- AJAX/JSON 请求返回 401 JSON避免 SPA 解析 HTML 崩溃 if accept:find(application/json) or x_req xmlhttprequest then return kong.response.exit(401, { message Unauthorized: Session expired or not authenticated }) end -- 浏览器请求 302 重定向到登录入口 local redirect_target conf.signin_url .. ?rd .. ngx.escape_uri(req_uri) return kong.response.exit(302, nil, { [Location] redirect_target, [Cache-Control] no-cache, no-store, must-revalidate, }) end return kong.response.exit(res.status, res.body) end return OAuth2ForwardAuth对应的schema.lua定义配置字段-- schema.lua local typedefs require kong.db.schema.typedefs return { name oauth2-forward-auth, fields { { consumer typedefs.no_consumer }, { protocols typedefs.protocols_http }, { config { type record, fields { { auth_url { type string, default http://oauth2-proxy.default.svc.cluster.local:4180/oauth2/auth }}, { signin_url { type string, default /oauth2/start }}, { timeout { type number, default 3000 }}, { keepalive_timeout { type number, default 60000 }}, }, }}, }, }3.4 Kong 插件注册与路由挂载把插件注册到 Kong Ingress Controller 的 Helm values 里# kong-controller values.yaml ingressController: installCRDs: false env: database: off gateway: enabled: true deployment: daemonset: true proxy: externalTrafficPolicy: Local plugins: configMaps: - pluginName: oauth2-forward-auth name: kong-plugin-oauth2-forward-auth然后定义 KongClusterPlugin让所有命名空间的 HTTPRoute 都能引用apiVersion: configuration.konghq.com/v1 kind: KongClusterPlugin metadata: name: oauth2-forward-auth plugin: oauth2-forward-auth config: auth_url: http://oauth2-proxy.default.svc.cluster.local:4180/oauth2/auth signin_url: /oauth2/start timeout: 30003.5 业务路由挂载与流量分流DbGate 的 Ingress 注解挂上插件annotations: konghq.com/plugins: oauth2-forward-authLiteLLM 需要做流量分流。推理 API 路径/litellm/v1/*不挂插件由 LiteLLM 自身校验 Bearer Token管理控制台路径/ui、/_next、/key、/user等挂上插件# 推理 API 路由不挂插件 route: enabled: true parentGateway: kong-main-gateway path: /litellm pathType: PathPrefix annotations: konghq.com/strip-path: true # 管理控制台路由挂插件 extraRoutes: ui-route: parentGateway: kong-main-gateway annotations: konghq.com/plugins: oauth2-forward-auth rules: - matches: - path: /ui - path: /_next - path: /key - path: /user - path: /team - path: /models - path: /spend4. 验证请求与成功结果配置同步完成后用 curl 验证三条链路。场景一未登录访问 Web 控制台预期 302 重定向。curl -s -I https://gw.yourdomain.com/dbgate/返回HTTP/2 302 location: /oauth2/start?rd%2Fdbgate%2F cache-control: no-cache, no-store, must-revalidate场景二未登录调用后台管理 API预期 401 JSON。curl -s -i -H Accept: application/json https://gw.yourdomain.com/key/list返回HTTP/2 401 content-type: application/json; charsetutf-8 {message:Unauthorized: Session expired or not authenticated}场景三外部 Agent 调用推理 API预期直接穿透到 LiteLLM。curl -s -X GET https://gw.yourdomain.com/litellm/v1/models返回{ error: { code: 401, message: Authentication Error, No api key passed in., type: auth_error } }这个 401 是 LiteLLM 自己返回的说明请求已经穿透 Kong 到达了 LiteLLM PodOAuth2-Proxy 完全没有参与。带上正确的 Bearer Token 后就能正常调用模型推理接口。5. 本篇常见错排查5.1 OAuth2-Proxy 报 500日志显示 email claim 缺失现象是登录回调时 OAuth2-Proxy 直接 500日志里出现email claim not found。原因是 GitHub 用户没有公开邮箱Logto 下发的 ID Token 里没有 email 字段。解决办法是在 OAuth2-Proxy 的环境变量里强制用sub作为身份标识- name: OAUTH2_PROXY_USER_ID_CLAIM value: sub - name: OAUTH2_PROXY_OIDC_EMAIL_CLAIM value: sub - name: OAUTH2_PROXY_EMAIL_DOMAINS value: * - name: OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL value: true5.2 跨命名空间插件引用失效如果 Kong 报错no KongPlugin or KongClusterPlugin was found for llm-system/oauth2-forward-auth说明你在 default 命名空间只创建了 KongPlugin但 llm-system 下的 HTTPRoute 引用不到。解决办法是创建 KongClusterPlugin集群级或者在每个业务命名空间内分别创建同名的 KongPlugin。5.3 Next.js SPA 白屏LiteLLM 的 UI 是 Next.js 单页应用。如果只保护/ui路径静态资源/_next/*和后台接口/key/*、/user/*会裸露在公网。更严重的是Session 过期时后台 AJAX 请求收到 302 HTML前端 JSON 解析失败导致白屏。解决办法是在 Lua 插件里判断Accept: application/json或X-Requested-With: XMLHttpRequest命中时直接返回 401 JSON让前端优雅地触发刷新。5.4 Cookie 跨子域不生效如果网关域名是gw.yourdomain.com而 OAuth2-Proxy 的 Cookie 域设为默认登录后跳回业务页面仍然未认证。需要设置- name: OAUTH2_PROXY_COOKIE_DOMAINS value: .yourdomain.com这样 Cookie 才能在所有子域下共享。5.5 插件优先级冲突Kong 的插件执行顺序由PRIORITY决定。如果同时挂了多个插件Forward-Auth 的优先级要设得足够高比如 1001确保它在其他业务插件之前执行。否则可能出现请求已经被转发到后端才想起来要鉴权的情况。6. 继续接入与验证整套链路跑通后你可以先到 TaoToken 模型对话 页面验证模型是否正常响应确认上游 API 可用。如果需要在 LiteLLM 里配置多个模型提供方记得把 TaoToken 的api_base设为https://taotoken.net/apiKey 用你在控制台创建的那串sk-开头的字符串。对于需要长期跑 Coding Agent 的场景建议到 Coding Plan 了解针对高频编码调用的优化方案。如果你在配置 Kong 插件或 OAuth2-Proxy 时遇到报错可以先到 API Keys 确认 Key 状态再对照 接入文档 检查参数格式。最后提醒一点Logto 的应用级访问控制RBAC可以在不修改任何 K8s 配置的情况下为不同成员分配不同应用的访问权限。管理员拥有 DbGate 和 LiteLLM 的完整权限普通成员只分配 LiteLLM UI 权限点进 DbGate 直接被拒。所有权限变更在 Logto 界面点选完成即改即生效不需要重启任何服务。
返回列表