Day 14 / 共 20 天 · 第 3 周 插件系统

鉴权与 consumer

"谁能访问我的 API"是网关核心能力。今天串起鉴权全流程:consumer 数据怎么加载、key-auth 插件怎么在 rewrite 阶段识别身份、attach_consumer 怎么把 consumer 挂到 ctx 上触发 Day 04 的二次合并。

📍 你在整条链的位置(第 3 周 插件系统)
配置合并 D13 鉴权与 consumer(认出"你是谁") 典型插件 D15 服务发现 D16
L01

鉴权的位置

🤔 痛点:谁都能调我的 API 吗? 付费接口被白嫖、内部接口被外人打——网关必须先分清"来的是谁、有没有资格",才谈限流、计费、按人配额。
💡 本质:consumer = 会员卡,鉴权 = 门卫查卡 consumer 就是网关给每个调用方发的"会员卡"(alice、bob、某合作方 App)。鉴权插件小区门卫:你出示凭据(key/JWT/账号密码)→ 门卫查会员名册 → 认出你是 alice,把"alice"这张身份牌别在你身上(挂到 ctx)。后面的限流、日志、计费一看身份牌就知道该怎么对待你。认不出 + 不允许匿名 → 直接挡在门外(401)。

鉴权插件(key-auth/jwt-auth/basic-auth…)都实现 rewrite 方法(priority 很高,如 key-auth),在 Day 04 的第一轮 rewrite 就跑——早早确认身份。

为什么鉴权在 rewrite 且 priority 高? 鉴权是"门禁"——必须在其他处理(限流、改写、转发)之前完成,否则会给未授权请求做无用功、甚至泄露。所以鉴权插件 priority 排在最前(Day 11),在 rewrite 阶段最先跑。而且鉴权识别出 consumer 后,后续插件才能"按身份定制"(Day 04 的二次合并)——顺序上必须靠前。这就是 priority 设计的意义:保证插件按正确顺序协作。
L02

consumer 的 init_worker

consumer.lua:300init_worker watch etcd 的 /consumers(Day 07),把所有 consumer 同步进内存。consumers_kv:263)按"插件名 + key 字段"建索引,方便按 key 快速查。

读法:consumer 数据在内存里按 (plugin_name, key) 组织成查找表——比如 key-auth 的所有 consumer 按它们的 key 值建索引。这样请求带一个 key 来,能 O(1) 查到对应 consumer。存入前还会 check_consumer:284)用 schema 校验(Day 09)。
L03

key-auth rewrite

plugins/key-auth.lua:104-122

function _M.rewrite(conf, ctx)
    local consumer, consumer_conf, err = find_consumer(ctx, conf)  -- 按请求里的 key 查
    if not consumer then
        if not conf.anonymous_consumer then
            return 401, { message = err}     -- 没找到且不允许匿名 → 拦截 401
        end
        -- 允许匿名:降级到匿名 consumer
        consumer, consumer_conf, err = consumer_mod.get_anonymous_consumer(conf.anonymous_consumer)
        if not consumer then return 401, { message = "Invalid user authorization"} end
    end
    consumer_mod.attach_consumer(ctx, consumer, consumer_conf)   -- 识别成功,挂到 ctx
end
门卫查会员卡find_consumer 查到 alice attach_consumer → 别上身份牌,放行 查不到 + 不许匿名 return 401 拦下 查不到 + 允许匿名 当成匿名 consumer(低配额)继续
key-auth 的三条岔路:认出→放行、认不出→401、认不出但允许匿名→降级匿名。
鉴权的三种结局 ① 找到 consumer → attach(成功,请求带上身份继续);② 没找到 + 不允许匿名 → 返回 401(拦截,Day 12 的"返回 code 即拦截");③ 没找到 + 允许匿名 → 用匿名 consumer 继续(给未登录用户一个受限身份)。key-auth 的 key 从请求头/query 取(schema 里配 header/query 名)。其他鉴权插件(jwt/basic/hmac)流程一样,只是"怎么验凭据"不同。
L04

find_consumer

consumer.lua:271-282

function _M.find_consumer(plugin_name, key, key_value)
    local consumer_conf = _M.plugin(plugin_name)         -- 该鉴权插件的所有 consumer
    if not consumer_conf then return nil, nil, "Missing related consumer" end
    local consumers = _M.consumers_kv(plugin_name, consumer_conf, key)  -- 按 key 建的索引
    local consumer = consumers[key_value]                -- 用请求带的 key 值查
    return consumer, consumer_conf
end
读法:纯内存查找——从"按 key 索引的 consumer 表"里用 key_value(请求带来的实际 key)直接取。不查 etcd、不查数据库,内存 O(1)。这就是为什么 consumer 要在 init_worker 时建好索引(L02)。查到就是认证成功。
📝 举个例子 建了 consumer alice,key-auth 的 key = "abc123"。请求带 apikey: abc123 来 → find_consumer 用 key_value="abc123" 在索引表里一查 → 命中 alice → ctx.consumer = alice
换成 apikey: wrong → 表里查不到 → 返回 401 {"message":"Invalid API key..."}

👶 小白:每来一个请求都要去 etcd 查一遍会员卡吗?那不慢死?

👨‍🏫 老师:不。所有 consumer 在 worker 启动时(L02 init_worker)就从 etcd 同步进内存、按 key 建好了查找表(像门卫开工前先把会员名册背进脑子)。请求来了纯内存 O(1) 查表,不碰 etcd。etcd 那边有人改了 consumer,watch(Day 07)会自动更新这张内存表。

L05

attach_consumer

consumer.lua:213attach_consumer(ctx, consumer, conf):把识别出的 consumer 挂到 ctx.consumer(和 consumer_name/group_id 等)。

这一挂,触发 Day 04 的连锁反应 回想 Day 04:第一轮 rewrite 跑完后,代码检查 if api_ctx.consumer then ...——就是这里 attach 的。一旦挂上 consumer,就触发"合并 consumer 专属配置 + 补跑 rewrite_in_consumer"(Day 12-13)。所以鉴权插件的职责就是"识别身份并 attach",之后的"按身份定制"由框架自动接管。attach 后,后续插件(限流等)和日志都能拿到 ctx.consumer 知道"这是谁"。
L06

匿名 consumer

consumer.lua:335get_anonymous_consumer:为"没带凭据"的请求提供一个预设的匿名身份。

匿名 consumer 的用途 有些 API 想"登录用户走高配额、匿名用户走低配额"而不是直接拒绝匿名。配了 anonymous_consumer 后,没凭据的请求不返回 401,而是当成匿名 consumer——它可以有自己的限流等配置(比如匿名 10 QPS、登录 100 QPS)。这比"要么全拒要么全放"灵活。本质还是走 consumer 那套(匿名也是个 consumer),复用同一机制。
L07

其他鉴权插件

APISIX 的鉴权插件家族(都在 plugins/,都实现 rewrite + attach_consumer 套路):

  • key-auth:API Key(最简单)
  • jwt-auth:JWT token 验证
  • basic-auth:HTTP Basic 用户名密码
  • hmac-auth:HMAC 签名
  • ldap-auth / openid-connect:对接 LDAP / OIDC
读法:它们的差异只在"怎么验凭据"(Day 09 各有自己的 schema 和验证逻辑),识别出 consumer 后都调 attach_consumer——后续流程完全一致。回想上一站 Higress Console 只支持 key-auth(Day 13),APISIX 的鉴权生态丰富得多。这体现了"内核统一(consumer 机制)+ 插件多样(各种验证方式)"的插件化威力。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么鉴权在 rewrite 且 priority 高?
  • find_consumer 怎么内存 O(1) 查?consumer 何时建索引?
  • attach_consumer 触发了 Day 04 的什么连锁反应?
  • 匿名 consumer 的用途?多种鉴权插件的共性与差异?

✋ 动手

cd /Users/bitmart/work/codes/github/apisix
sed -n '104,122p' apisix/plugins/key-auth.lua
sed -n '213,300p' apisix/consumer.lua
ls apisix/plugins/ | grep -E "auth"
明天预告 · Day 15(第 3 周收官)典型插件精读——挑三个代表性插件读透:limit-count(限流)、proxy-rewrite(改写)、ai-proxy(AI 代理),看它们怎么把前面学的机制落地成真实功能。
← Day 13 Day 15 · 典型插件 →