目标:不依赖 Kubernetes,只用 Docker 在本地完整复现 “McpBridge + Ingress + WasmPlugin(key-auth)” 三件套的运行效果(401 / 403 / 200 / 404),并讲清背后的原理。
本文所有操作已在 macOS + Docker Desktop 上实测通过(2026-09-17)。
目录
一、背景:要复现的原配置
原始配置是三个 K8s 资源(完整内容见 citic-wasm-plugin/key-auth/keyauth.yaml):
| 资源 | 作用 | 关键内容 |
|---|---|---|
McpBridge mcp-keyauth-httpbin | 把集群外的 httpbin.org:80 注册进网关,服务名为 httpbin.dns | type: dns,网关周期性做 DNS 解析得到 IP 池 |
Ingress ingress-keyauth-httpbin | 路由规则:Host: httpbin.example.com 的流量全部转发到 httpbin.dns | higress.io/destination、upstream-vhost、backend-protocol 三个注解 |
WasmPlugin wasm-keyauth-httpbin | 在该 Ingress 上启用 key-auth 认证 | 2 个 consumer,只放行 consumer1;从 x-api-key / apikey 头取 Key |
引用关系:Ingress.backend.resource → McpBridge 名;annotations.higress.io/destination → name.type(httpbin.dns);WasmPlugin.matchRules[].ingress → Ingress 名。
二、总体方案与架构
K8s 里这套东西最终也是由 Higress 控制面翻译成 Envoy 配置下发的。所以我们绕过控制面,直接手写等价的 Envoy 静态配置,用 Docker 跑数据面:
| |
相比原方案的两处替换:
- 上游替换:
httpbin.org(公网)→ 本地mccutchen/go-httpbin容器。行为等价(都是请求回显服务),且不依赖外网。 - 控制面替换:McpBridge/Ingress/WasmPlugin 三个 CRD → 一份
envoy.yaml。三者的语义被完整保留(域名匹配、路由名、插件配置、allow 白名单)。
所有文件在 key-auth-docker-demo/ 目录:
2.1 与官方独立部署方式的关系(先读这里)
Higress 官方文档提供了三种"脱离/本地化 K8s"的部署方式,它们和本文方案不是一回事,定位不同:
| 方式 | 组件规模 | 配置方式 | 适用场景 | 官方文档 |
|---|---|---|---|---|
| All-in-One 单容器 | 1 个容器(gateway+console 打包) | 控制台 UI / 本地 /data 卷 | 快速体验完整网关功能 | deploy-by-docker |
| Docker Compose 独立部署 | apiserver + controller + pilot + gateway + console(可选内置 nacos、Prometheus/Loki/Grafana 监控套件),约 12 个容器 | 控制台 UI / 模拟 K8s API / 本地配置文件 | 接近真实架构的本地测试 | deploy-by-docker-compose |
| 本地 K8s(kind)+ Helm | 完整云原生模式 | kubectl apply CRD(本文开头那份 YAML 可直接用) | 与生产完全一致的验证 | quickstart |
| 本文方案:裸数据面 | 2 个容器(envoy + httpbin) | 手写 envoy.yaml | 只关心插件本身的行为和原理,最轻最快 | 本文 |
本文方案可以理解成:把官方架构中 pilot 最终下发给 gateway 的那份 Envoy 配置"手写"了出来,只跑数据面,不跑控制面。对"验证 key-auth 插件的认证行为(401/403/200)“这个目的来说,2 个镜像、一条 docker compose up -d 就够了。
官方两种方式的核心命令(引自官方文档,供需要完整控制台时使用):
| |
实测备注(2026-09-17 本机实际尝试过 compose 独立部署后的经验,供参考):
- 重:约 12 个镜像(apiserver、controller、pilot、gateway、console、plugin-server、nacos、prometheus、loki、grafana 等),首次安装拉取超过 30 分钟;日常验证插件没必要。
-c file:///opt/higress/conf默认会把配置写到宿主机/opt/higress(macOS 上直接Permission denied),要改成工作区内的绝对路径。- 安装脚本内部用
docker compose -p higress管理容器;如果自己在compose/目录里手动执行docker compose而不带-p higress,会另建一套compose-*前缀的重复容器并抢 80 端口报错。 - 一个利好:compose 部署内置
plugin-server容器,自带 key-auth 2.0.0 等官方插件,WasmPlugin.url直接写http://plugin-server:8080/plugins/key-auth/2.0.0/plugin.wasm即可,不需要自己编译 wasm——但那样跑的就不是本仓库的代码了。 - 卸载干净的方法:安装目录下
bash bin/reset.sh(停容器、删数据卷),再删安装目录和镜像。
结论:想体验控制台和完整控制面 → 用上面官方方式;想验证/调试本仓库的插件代码、搞清楚数据面到底发生了什么 → 用本文方案。
三、原理介绍
3.1 WASM 插件是怎么跑起来的
- Higress 的数据面是 Envoy 的一个 fork。Envoy 内置
envoy.filters.http.wasm过滤器,可以把一个.wasm文件加载进 V8 运行时,作为 HTTP 过滤器链的一环执行。 - 插件本身是用 Go 写的(
citic-wasm-plugin/key-auth/main.go),基于higress-group/proxy-wasm-go-sdk,通过GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared编译成 wasm。 - 插件通过
wrapper.SetCtx("key-auth", ...)注册三类回调:ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig)— 插件启动/配置变更时解析配置ProcessRequestHeadersBy(onHttpRequestHeaders)— 每个请求到达时执行认证判断
- 请求流程:Envoy 收到请求 → 路由匹配(host + path)→ 执行 wasm 过滤器(key-auth 认证)→ 认证通过则转发给上游 cluster,并把
X-Mse-Consumer: <consumer名>头注入给上游;不通过则由插件直接返回本地响应(401/403),请求根本不会到达上游。
插件里发回本地响应用的是 proxywasm.SendHttpResponseWithDetail(main.go:329-345),同时带上 WWW-Authenticate: Key realm=MSE Gateway 响应头——这就是实测输出里 www-authenticate: Key realm=MSE Gateway 的来源。
3.2 Proxy-Wasm ABI 与 0_2_100 的坑
Envoy 与 wasm 插件之间通过 Proxy-Wasm ABI(一组以 proxy_ 开头的导入/导出函数)通信。Envoy 加载 wasm 时会检查模块导出的 ABI 版本号函数:
- 官方 Envoy 认
proxy_abi_version_0_2_1 - 用官方 C++ SDK 编译的插件(如官方镜像
plugins/key-auth:1.0.0里的 wasm)导出的就是proxy_abi_version_0_2_1 - 但 Higress 的 Go SDK fork 导出的是
proxy_abi_version_0_2_100(Higress 私有扩展 ABI,增加了 redis 调用等能力)
验证方法(无需反编译器,strings 即可):
结论:Go 源码编译的 key-auth 插件只能跑在 Higress fork 的 Envoy 上。如果挂载到官方 envoyproxy/envoy,启动即失败:
所以本方案使用 higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.2.4(内含 Envoy 1.36.4 的 Higress fork),通过覆盖 entrypoint 直接裸跑 envoy:
| |
3.3 key-auth 插件的认证逻辑
核心逻辑在 main.go 的 onHttpRequestHeaders(main.go:239)。插件行为由两个变量决定:
global_auth:全局配置里的开关(true / false / 未设置,三种状态)- 当前路由是否配置了
allow列表(即 matchRules 是否命中当前路由)
决策真值表:
| global_auth | 当前路由有 allow | 行为 |
|---|---|---|
| true | 否 | 全局限流:所有 consumer 的 Key 都可用 |
| true | 是 | 只有 allow 列表里的 consumer 可用 |
| false | 否 | 直接放行,不认证 |
| false | 是 | 只有 allow 列表里的 consumer 可用 |
| 未设置 | 全部路由都没配 | 退化为全局认证 |
| 未设置 | 至少一个路由配了 | 等价于 false:没配的路由放行,配了的路由按 allow 校验 |
本次复现的配置是 global_auth: false + 路由配 allow: [consumer1],即上表第 4 行。
取 Key 与判定顺序(main.go:259-326):
- 按
keys列表从 Header(in_header: true)或 URL query(in_query: true)取 token - 取到多个 token → 401(
deniedMultiKeyAuthData) - 一个都没取到 → 401(
deniedNoKeyAuthData,body:No Key Authentication information found) - token 在 consumers 里查不到 → 403(
deniedUnauthorizedConsumer,body:Unauthorized consumer) - 查到 consumer 后先注入
X-Mse-Consumer: <name>头,再检查是否在allow列表:- 不在 → 403(
deniedUnauthorizedConsumer) - 在 → 放行,请求转发上游
- 不在 → 403(
⚠️ 注意:假 Key 和"不在白名单的有效 Key"都是 403,只有"完全不带 Key"或"带多个 Key"才是 401。原 YAML 讲解文档里说假 Key 返回 401 是不准确的,以源码和实测为准。
3.4 规则匹配:rules 与 match_route
K8s 里 WasmPlugin.matchRules[].ingress: [ingress-keyauth-httpbin] 经控制面翻译后,落到 wasm 插件配置里是:
| |
wasm-go SDK 的 wrapper 在每次请求时取 Envoy 的 route_name 属性,和 _match_route_ 列表比对:命中则用该条规则的配置(覆盖全局配置中的 allow),不命中则用全局配置。
因此在裸 Envoy 配置里,我们只要给路由起个名字(name: keyauth-route),并在插件配置里写 _match_route_: ["keyauth-route"],就等价复现了 matchRules 的语义。
3.5 与原 YAML 的字段对应关系
| 原 YAML 字段 | envoy.yaml 中的等价物 |
|---|---|
Ingress rules[0].host: httpbin.example.com | virtual_hosts[].domains: ["httpbin.example.com"] |
path: / + pathType: Prefix | routes[].match.prefix: / |
McpBridge(httpbin.org:80, type: dns) | clusters[].name: httpbin + STRICT_DNS 指向本地 httpbin 容器 |
higress.io/upstream-vhost: httpbin.org | 本地回显服务不校验 Host,省略;真实场景对应 host_rewrite_literal |
matchRules[].ingress | 路由命名 keyauth-route + 插件配置 _match_route_ |
defaultConfig(consumers/keys/in_header/global_auth) | 插件 JSON 配置的同名字段 |
matchRules[].config.allow: [consumer1] | _rules_[0].allow: ["consumer1"] |
envoy.yaml 中插件配置的完整 JSON(即 defaultConfig + matchRules 合并后的结果):
| |
四、搭建步骤(从零开始)
4.1 准备镜像
docker.io 不通时可走镜像加速器,例如 DaoCloud:
docker pull docker.m.daocloud.io/library/golang:1.24 && docker tag docker.m.daocloud.io/library/golang:1.24 golang:1.24。 注意mccutchen/go-httpbin不在 DaoCloud 白名单内,需直连 docker.io 或开代理;阿里云 registry(higress-registry.cn-hangzhou.cr.aliyuncs.com)国内直连无压力。
4.2 编译 plugin.wasm
用 Docker 编译本仓库的 Go 源码,不污染本地 Go 环境:
| |
产物约 5.4MB。gomodcache/gocache 两个 named volume 用于缓存依赖,二次编译会快很多。
备选:也可以直接从官方 OCI 镜像提取预编译 wasm(注意是 C++ 版本,ABI 为 0_2_1,可跑在官方 Envoy 上,但拒绝响应的文案与 Go 版不同):
1 2 3 4 5TOKEN=$(curl -s "https://dockerauth.cn-hangzhou.aliyuncs.com/auth?service=registry.aliyuncs.com:cn-hangzhou:china:cri-r0xfyoxudmtseqq7&scope=repository:plugins/key-auth:pull" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") # 先 GET /v2/plugins/key-auth/manifests/1.0.0 找到 wasm 层的 digest,再: curl -sL -H "Authorization: Bearer $TOKEN" -o plugin.wasm \ "https://higress-registry.cn-hangzhou.cr.aliyuncs.com/v2/plugins/key-auth/blobs/<digest>"
4.3 编写配置
envoy.yaml 和 docker-compose.yaml 已就绪(见 key-auth-docker-demo/),关键点:
- 监听
0.0.0.0:10000,宿主机映射10000:10000 admin开在 9901 端口(可选,用于看配置和统计)- wasm 过滤器配置中
runtime: envoy.wasm.runtime.v8,code.local.filename: /etc/envoy/plugin.wasm plugin.wasm以只读方式挂载进容器
4.4 启动与验证
五、验证结果(实测输出)
① 不带 Key → 401
② consumer1 的 Key(在白名单)→ 200
③ consumer2 的 Key(有效但不在 allow)→ 403
④ 假 Key → 403(不是 401!)
credential 在 consumers 里查不到,与"不在白名单"走同一个分支(main.go:288-292)。
⑤ Host 不匹配 → 404
| |
| |
请求根本没命中 httpbin.example.com 这个 virtual host,路由不存在,与插件无关。
六、踩坑记录
官方 Envoy 加载不了 Go 编译的插件:报
Missing or unknown Proxy-Wasm ABI version。原因是 Higress Go SDK 导出proxy_abi_version_0_2_100,官方 Envoy 只认0_2_1。解法:换higress/gateway镜像。(详见 3.2)docker pullOCI wasm 插件镜像报unsupported media type application/vnd.oci.empty.v1+json:wasm 插件镜像是FROM scratch+ OCI 空层的纯 artifact,Docker 旧存储驱动不支持。解法:要么在 Docker Desktop 设置里启用 containerd 存储,要么绕过 daemon 直接 curl registry API 下载 blob(见 4.2 备选方案)。docker.io 拉取超时:国内直连 docker.io 经常不通。阿里云 registry 不受影响;其余镜像走加速器或代理。
原始文档的错误:网上流传的该 YAML 讲解中"假 Key 返回 401"的说法与 Go 实现不符,实际是 403。读插件源码(
main.go的四个denied*函数)是最可靠的事实来源。
七、常用命令速查
| |
附:单元测试路线(不需要 Docker)
如果只想快速验证插件逻辑的正确性,不追求真实 HTTP 效果,可以直接跑仓库里自带的单元测试(main_test.go,基于 wasm-go 的 host 模拟框架,wazero 在进程内跑 wasm,不需要 Docker):
它覆盖了配置解析(合法/非法配置)、Header/Query 取 Key、401/403 分支、X-Mse-Consumer 注入等场景。但它验证的是"插件内部逻辑”,而不是"网关端到端行为"——后者还是要靠本文的 Docker 方案。
