Day 13 / 共 20 天 · 第 3 周 Wasm 插件

写一个插件(key-auth)

昨天(Day 12)把 SDK 的骨架(SetCtx/三级 Context/生命周期)过了一遍;今天把它用起来——逐行读一个真实的认证插件 key-auth(API Key 鉴权)。它是典型的 auth 插件,展示"配置解析 + 请求头处理 + 拦截"的完整套路,比 hello-world 实用得多。

📍 你在第 3 周(Wasm 插件)的位置
D11 插件概览 D12 wasm-go SDK D13 写插件 key-auth D14 实战插件 D15 golang-filter
🤔 痛点:Day 12 学了 SDK 的一堆概念,但"真写一个有用的插件"到底长啥样? hello-world 只会加个头、回句 "hello world",玩具而已。真实插件要读配置、按规则匹配、拦非法请求、处理各种边界情况——这些拼在一起是什么样子?今天用一个天天在用的认证插件 key-auth 给你走一遍完整套路。
💡 先兜住今天(延续"门口保安"世界观) key-auth 就是门口那位查证件的保安:访客递上"门禁卡"(API Key,藏在请求 header 或 query 里),保安拿着手里的花名册credential2Name 反查表)一查——卡有效就放行、并在访客背上贴张"这是张三"的标签(X-Mse-Consumer 头)给楼里同事看;卡无效或没带卡,当场请回(返回 401/403)。今天就看这位保安从"读花名册(配置)"到"查卡放行/拦截"的完整工作流程。
L01

key-auth 是什么

extensions/key-auth/main.go(404 行)——API Key 认证:检查请求带的 key 是否合法,合法放行并标记调用方身份,非法拦截返回 401/403。

API Key 鉴权 = "查通行证" 很多 API 用 API Key 认证:客户端在 header(或 query)带一个 key,网关检查 key 是否有效。key-auth 插件就干这个:从请求里取 key → 查合法 key 列表 → 合法则加个 X-Mse-Consumer header 标记"这是谁"、放行;非法则直接返回 401(没带 key)/403(key 错)。这是网关最常见的能力之一。读它能学会"一个真实插件从配置到拦截的完整套路"——比 hello-world 实用得多。
L02

注册回调

// key-auth/main.go:38 init 注册两个回调
wrapper.SetCtx("key-auth",
  wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig),  // 全局+规则配置解析
  wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),                       // 处理请求头
)
读法:注册两样:①ParseOverrideConfigBy——两个函数(全局配置解析 + 规则级覆盖解析,配合 Day 12 的 RuleMatcher 多级配置);②ProcessRequestHeadersBy——请求头到达时的处理逻辑。key-auth 只需处理请求头(鉴权在头阶段就能完成,不用看 body)。
L03

配置结构 + 注解

// key-auth/main.go:95 KeyAuthConfig(含大量 @Title/@Description/@Scope 注解)
// :67 顶部:@Name key-auth / @Category authn / @Phase AUTHN / @Priority 321
type KeyAuthConfig struct {
  consumers []Consumer    // 合法的 key 及其身份
  keys []string           // key 在哪个 header/query 名
  inQuery, inHeader bool  // key 从 query 还是 header 取
  // …
}
注解自动生成"配置表单" 注意配置结构体上大量 @Title/@Description/@Scope GLOBAL 注解。Higress 用这些注解自动生成插件市场的配置表单和文档——用户在 Console(下一站)里看到的插件配置界面就是从这些注解生成的。顶部的 @Phase AUTHN/@Priority 321 声明"这是个认证阶段插件、优先级 321"(对应 Envoy 课 Day 17 的 WasmPhase/WasmPriority——认证插件在别的插件之前跑)。这是"代码即文档即 UI"的实践——注解一处定义,多处复用。
L04

parseGlobalConfig

// key-auth/main.go:140 parseGlobalConfig
// 校验 keys、in_query/in_header、consumers
// 构建 credential2Name 反查表(key → consumer 名字)
读法:解析全局配置:校验必填项,把"合法 key 列表"预处理成 credential2Name 反查表(key → 是谁)。预建反查表是性能优化——运行时鉴权直接查表 O(1),不用遍历。你的 ParseConfig 回调(Day 12)就长这样——把 JSON 配置转成运行时高效的结构。
L05

覆盖配置继承

// key-auth/main.go:245 parseOverrideRuleConfig
*config = global   // 先继承全局配置
// 再解析路由级 allow 列表(这个路由允许哪些 consumer)
规则级配置"继承"全局 Day 12 讲了 RuleMatcher 的多级配置。这里体现"继承":路由级配置先 *config = global(拷贝全局配置作基础),再叠加这个路由特有的设置(比如"这个路由只允许 consumer A、B 访问")。于是全局定义"有哪些合法 key",路由级定义"这个路由允许哪些 key"——分层且不重复。这就是 Day 12 的 ParseOverrideConfig(全局 + 规则两个函数)的实际用法。
L06

onHttpRequestHeaders

// key-auth/main.go:278 onHttpRequestHeaders
// 从 header/query 取 token(:299)
// credential2Name 校验(:327)
// 通过 → AddHttpRequestHeader("X-Mse-Consumer", name)(:333,标记身份)
// 失败 → SendHttpResponseWithDetail 返回 401/403(:368,带 WWW-Authenticate 头)
读法:鉴权主流程:取 key → 查反查表 → 合法则加 X-Mse-Consumer header(告诉下游"这是谁")并 ActionContinue(放行);非法则 SendHttpResponseWithDetail 直接返回 401/403(拦截,不转发上游,对应 Envoy 课 Day 08 的 sendLocalReply)。这就是网关"当场拦截非法请求"的实现。
保安查证件的三条岔路(onHttpRequestHeaders) 请求到达 取 header/query 里的 key 查花名册 credential2Name 没带 key → 401 Unauthorized(当场请回) key 错/无权 → 403 Forbidden(当场请回) key 对 → 贴标签 X-Mse-Consumer: 张三 → ActionContinue 放行上游
图注:鉴权在"请求头阶段"就能拍板——放行/拦截,不用等 body。放行才转上游,拦截直接回响应。
📝 举个例子:两次请求,一次放行一次拦截 合法花名册 credential2Name = {"abc123": "张三", "xyz789": "李四"}
① 请求带 X-Api-Key: abc123 → 查到"张三" → 加头 X-Mse-Consumer: 张三放行,上游服务从这个头知道"来的是张三"。
② 请求带 X-Api-Key: hack000 → 花名册里没有 → SendHttpResponseWithDetail(403)拦截,请求根本到不了上游。
L07

三态鉴权语义

global_auth 的三态(真实工程细节) key-auth 有个 global_auth 配置的三态语义(main.go:266-277 注释是极好的教学材料):true——全局强制鉴权(所有路由都要 key);②false——只有显式配了 allow 的路由才鉴权;③未设(nil)——兼容模式,按是否有 consumer 配置决定。为什么要三态?因为要平滑兼容"老配置"和"新配置"、"全局开"和"按路由开"多种使用习惯。用 Go 的指针(nil 表示"未设")区分"设成 false"和"没设"。这种"三态布尔"是配置系统处理"默认 vs 显式"的常见手法(Istio 课 Day 19 的 BoolValue 也是)。读懂它,你就理解了真实插件如何处理配置的边界情况。
global_auth 的值类比(全楼门禁策略)效果
true整栋楼所有门都强制刷卡所有路由都要鉴权
false只有贴了"需刷卡"告示的门才查仅显式配了 allow 的路由鉴权
nil(没设)没下总政策,看各门自己有没有装读卡器兼容模式:按是否有 consumer 配置决定
⚠️ 常见误解:很多人以为"布尔就非真即假两种",于是 false 和"没设"会被当成一回事——那就错了。这里用 Go 指针 *boolnil 代表"运维压根没写这个字段",和显式写 false 语义不同。分清"没设"与"设成 false",才能做到平滑兼容老配置。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • key-auth 干什么?合法/非法各怎么处理?
  • 它注册了哪两个回调?
  • 配置结构的注解有什么用(自动表单)?
  • parseGlobalConfig 为什么建反查表?规则级怎么继承全局?
  • global_auth 三态各是什么?为什么用指针区分?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/higress/plugins/wasm-go/extensions/key-auth
sed -n '38,42p' main.go       # 注册
sed -n '266,340p' main.go     # 三态语义 + 鉴权主流程
sed -n '67,95p' main.go        # 注解 + 配置结构
明天预告 · Day 14实战插件——看更复杂的插件:Redis 分布式限流(cluster-key-rate-limit,用 leader 选举 + Redis)、transformer(改 header/body),以及插件的单测。
← Day 12 SDK Day 14 · 实战插件 →