场景

  • 安全评审要求消息中间件不再维护独立账号库,应用和运维人员统一走 Keycloak 认证;AMQP 客户端、管理 UI、HTTP API 三条入口都要覆盖。
  • 照着旧文章配了 auth_oauth2.issuer,客户端却连不上;日志里只有一句 invalid_token,看不出是签名、时间还是 audience 的问题。
  • 想知道 Keycloak 里的角色能不能直接变成 RabbitMQ 的 vhost 权限,省掉在 RabbitMQ 里另建一套权限表。

本文只覆盖 RabbitMQ 官方插件 rabbitmq_auth_backend_oauth2 与 Keycloak 的组合。基线是当前最新发布版 RabbitMQ 4.3.6(2026-09-14)与官方文档的 4.3 版;所有配置键、默认值和报错文案都取自官方 OAuth 2.0 文档、OAuth 2 排错文档与官方 rabbitmq-oauth2-tutorial 的 Keycloak realm 配置。托管发行版与云服务的 OAuth 封装(例如 VMware Tanzu RabbitMQ 的 forward proxy 能力)属于商业特性,不在本文范围内。

适用与不适用

场景是否适用说明
应用(producer/consumer)用 Keycloak 服务账号连 AMQP✅本文主场景,走 client_credentials
运维人员用 Keycloak 登录管理 UI(SP 发起登录)✅需要另一个 public client,见第 2.1 节
用 Keycloak 角色/组直接驱动 vhost 权限✅靠 scope_aliases 或 additional_scopes_key 映射,见第 5 节
按用户生成不同的队列权限(多租户)✅用 scope 变量展开 {user_name} / {sub},见第 4.3 节
只想要「能连上」,本地开发图省事⚠️OAuth 链路要 IdP、证书、可达性三件齐;本地调试用 internal 后端更快,但别把配置带进预发
客户端不支持在连接中途换 token,且要做长连接消费⚠️AMQP 0.9.1 不主动断连但到期后拒绝操作;客户端要么支持 update-secret,要么自己重连,见第 7 节
已经用 LDAP/AD 做认证,只是想让 RabbitMQ 认同一批账号❌直接用 rabbitmq_auth_backend_ldap 成本更低;引入 OAuth 只是把 IdP 变成中间人

1. 认证链路:一次握手完成认证和授权

  sequenceDiagram
    participant A as AMQP 客户端(应用 / 管理 UI 浏览器)
    participant K as Keycloak(realm: iam)
    participant M as RabbitMQ(rabbitmq_auth_backend_oauth2)

    A->>K: POST /realms/iam/protocol/openid-connect/token<br/>grant_type=client_credentials
    K-->>A: access_token(scope 与 aud 由 client scope / mapper 决定)
    A->>M: AMQP 握手,access_token 放在 password 位置(username 被忽略)
    M->>K: GET /realms/iam/.well-known/openid-configuration(首次、未知 kid)
    M->>K: GET jwks_uri
    M->>M: 验签 → 校验 exp → 校验 aud 含 resource_server_id
    M->>M: 把 scope 翻译成 vhost 级 configure/read/write + tag
    M-->>A: 认证通过,身份与权限同时确定
    Note over A,M: token 到期后:AMQP 0.9.1 不断连但拒绝后续操作

这张图和 Kafka 的 OAUTHBEARER 有一个本质区别:RabbitMQ 的 OAuth 插件不只是一个认证器,它同时是授权引擎。 它不发任何请求给 Keycloak 做 introspection,而是把 token 里的 scope 直接翻译成 RabbitMQ 内部的 vhost 权限模型(configure / read / write 三个维度)和管理插件 tag(administrator / management / monitoring / policymaker)。

这个设计带来两个后果,也是本文所有排错的出发点:

  1. 权限不落在 RabbitMQ 的数据库里,而是随 token 走。 撤权 = 让下一个 token 不再带那个 scope,不需要在 RabbitMQ 侧改任何东西;反过来,已经签发的 token 在有效期内依然是有效的,user 表里也看不到任何痕迹。
  2. token 里没有的 scope 就是没有权限。 「认证通过但什么都做不了」不是 bug,是 scope 缺失,而 AMQP 层的报错往往只说权限不足,不会告诉你是 scope 拼错了。

1.1 aud 必须包含 resource_server_id,且这个值由 mapper 决定

RabbitMQ 默认校验 aud(auth_oauth2.verify_aud 默认 true):aud 必须等于 resource_server_id,或者是一个数组且包含该值。

注意这里和 Kafka 的差别:Kafka 的 expected.audience 是精确字符串比对,aud 里多一个值就通不过;RabbitMQ 是「包含」语义,对多资源场景更宽容。

Keycloak 的 access token 里 aud 不由你配的 resource_server_id 决定,而是由协议 mapper 决定,而 Keycloak 的默认 mapper 不会替你补上 rabbitmq:Audience Resolve 只把「该用户拥有其客户端角色的那些 client_id」加进 aud,并且显式跳过 client 自身,而服务账号在 Keycloak 侧一般没有任何客户端角色,这条 mapper 对服务账号等于不产生 aud。必须显式加一个 Audience mapper,并且值填在 included.custom.audience 上:

Keycloak Admin Console → Client scopes → <你的 scope> → Mappers → Add mapper → Audience
  Included Custom Audience = rabbitmq      ← 等于 auth_oauth2.resource_server_id
  Add to access token = On

官方教程仓库的 realm 导出里,面向 resource_server_id = rabbitmq 的那些 client(producer、mgt_api_client、rabbitmq-client-code …)都带一个这样的 oidc-audience-mapper,included.custom.audience 都是 rabbitmq;同一份导出里还有一组多资源示例(dev_producer、prod_producer、rabbit_dev_mgt_ui…),它们的值是 rabbit_dev / rabbit_prod——正好说明这个字段必须和对应的 resource_server_id 逐字符一致。这不是可选项,是必需品。

included.client.audience 是另一个字段,语义是「另一个 Keycloak client 的 client_id」——只有当 RabbitMQ 的 resource_server_id 恰好等于某个 Keycloak client 名时才用它。RabbitMQ 本身不是 Keycloak 的 client,所以别填错字段。关于这个 mapper 的取值优先级和轻量级 access token 模式下的开关差异, Kafka 那篇第 1.1 节有源码级说明,两个插件读的是同一个 Keycloak mapper,结论完全通用。

2. Keycloak 端:client scope 的名字就是 RabbitMQ 的权限

这是 RabbitMQ 这套集成最反直觉、也最省事的地方:你不需要写任何 claim、不需要自定义 mapper 来塞权限。创建一个名字等于 RabbitMQ scope 的 client scope,分配给 client,Keycloak 就会把这个名字原样放进 access token 的 scope claim。

官方教程的 realm 导出里,realm 级的 client scope 目录是这样的(节选,include.in.token.scope 均为 true):

client scope 名作用
rabbitmq.read:*/*任意 vhost、任意资源的读权限
rabbitmq.write:*/*任意 vhost、任意资源的写权限
rabbitmq.configure:*/*任意 vhost、任意资源的配置权限
rabbitmq.tag:administrator管理插件 administrator tag
rabbitmq.tag:management管理插件 management tag
rabbitmq.configure:*/q-{user_name}只允许配置以自己用户名开头的队列(变量展开)

名字里的 :、/、*、{} 都是合法字符,Keycloak 不做校验——这意味着拼错 scope 名不会有任何报错,只会安静地不产生权限。这一点比配置错误更难查,因为两边都不会报错。

另外两个必须注意的 Keycloak 侧开关:

  • Default vs Optional client scope。 这是 Keycloak 侧的语义:Default Client Scope 随每次取 token 自动授予,Optional 只有在授权/取 token 流程里显式请求才授予。应用侧通常不会自己拼 scope= 参数,所以官方教程把 rabbitmq.* 这些 scope 全部挂在 client 的 Default Client Scopes 上(producer、mgt_api_client 的 realm 导出里都是 Default,不是 Optional)。挂错成 Optional,token 里就没有权限,而两边都不报错。
  • include.in.token.scope 必须为 On。 这是控制台里 client scope 上那个「Include in token scope」开关,关掉之后 scope 名不会进 token 的 scope claim。

2.1 管理 UI 和后端应用要用两个不同的 client

这是最容易混的一处,官方文档写得很直白但很容易被忽略:

用途client 类型需要的开关
应用连 AMQP / HTTP API(client_credentials)confidentialClient authentication = On、Service accounts roles = On;Standard flow / Implicit flow / Direct access grants 全部关闭
运维用浏览器登录管理 UI(SP 发起登录)publicClient authentication = Off、Standard flow = On,不要配 client secret
只需要 curl 换个 token 做调试public 或 confidential 都行开 Direct access grants 才能用密码模式换 token

官方文档明确要求管理 UI 用「public web application」而不是 confidential:management.oauth_client_id 指向的 client 若要 client secret,浏览器侧根本无从提供(UAM 等个别 IdP 除外)。

管理 UI 的回调地址就是管理界面自身的 URL,具体形式不要凭猜——打开浏览器开发者工具的网络面板,在一次真实登录里看 redirect_uri 参数的原文,再回填 Keycloak 的 Valid redirect URIs。

2.2 三种 client 的最小配置清单

Realm:  iam

# ① 后端应用(每个应用一个)
Client: rabbitmq-app(confidential)
  Client authentication        = On
  Service accounts roles       = On
  Standard / Implicit / Direct = 全 Off
  Client scopes(Default)     = rabbitmq.read:*/*、rabbitmq.write:*/*
  Mapper                       = Audience → included.custom.audience = rabbitmq

# ② 管理 UI(整个集群一个)
Client: rabbitmq-mgmt-ui(public)
  Client authentication        = Off
  Standard flow                = On
  Client scopes(Default)     = rabbitmq.tag:management
  Mapper                       = Audience → included.custom.audience = rabbitmq
  Valid redirect URIs          = 以浏览器里实际出现的 redirect_uri 为准

# ③ 调试用(可选,用完即删)
Client: rabbitmq-debug(public,Direct access grants = On,带上面同样的 mapper)

第 ③ 个 client 只用来在排错时用密码模式快速换 token,验证「Keycloak 到底发出了什么 scope / aud」。它的存在本身就是风险(管理类账号可以用密码模式直接换 token),验证完就删。

2.3 确认 issuer

curl -s https://kc.example.com/realms/iam/.well-known/openid-configuration \
  | jq '{issuer, jwks_uri, token_endpoint}'

auth_oauth2.issuer 必须逐字符等于这里的 issuer。Keycloak 的 issuer 由 hostname/frontend URL 推导,内网地址、外网域名、代理头处理方式不同都会改变它——这类问题的完整边界在 Keycloak hostname v2 配置,在 RabbitMQ 侧的表现是 discovery 请求 404 或者验签一直失败。

还有一个只在生产出现的坑:RabbitMQ 节点必须能访问 auth_oauth2.issuer 这个地址。 如果 issuer 用的是外网域名,而 RabbitMQ 走内网,节点侧会发现不了 issuer——但浏览器侧的发现能成功(浏览器走外网)。症状是「浏览器能登录,应用一握手就失败」。这不是 OAuth 问题,是 DNS 分流问题,先确认节点里的 curl 能通再说别的。

3. RabbitMQ 端最小配置

# rabbitmq.conf
auth_backends.1 = oauth2                              # 别名,等于 rabbit_auth_backend_oauth2

auth_oauth2.resource_server_id = rabbitmq             # 必须出现在 token 的 aud 里
auth_oauth2.issuer = https://kc.example.com/realms/iam
auth_oauth2.https.cacertfile = /etc/rabbitmq/certs/idp-ca.pem   # IdP 用自签 CA 时必配

auth_oauth2.preferred_username_claims.1 = user_name
auth_oauth2.preferred_username_claims.2 = preferred_username

# 管理 UI 服务端发起的登录
management.oauth_enabled = true
management.oauth_client_id = rabbitmq-mgmt-ui
management.oauth_scopes = openid profile rabbitmq.tag:management
management.oauth_disable_basic_auth = false            # 保留管理 UI 的用户名密码入口,见第 3 节第 4 点

五处容易配错的地方:

  1. auth_backends.1 = oauth2 用的是扁平写法。 oauth2、oauth、完整模块名 rabbit_auth_backend_oauth2 三种写法等价,官方 Access Control 文档里有别名表。容易搞反的一点:auth_backends.1.authn / .authz 双段写法在 4.3 里依然有效,只是它的用途是「认证和授权走不同后端」(例如 authn = ldap + authz = internal);整条链都用 OAuth 时扁平写法就够。另外 Access Control 页面把 OAuth2 归在「只提供授权后端」的那一组,但插件源码里 rabbit_auth_backend_oauth2 同时声明了 -behaviour(rabbit_authn_backend). 与 -behaviour(rabbit_authz_backend).,并导出 user_login_authentication/2——它确实能同时承担认证与授权,按页面分类去理解会以为它做不了认证。

  2. 插件必须先启用。 配置里引用了 oauth2 后端,而 rabbitmq_auth_backend_oauth2 没有出现在 enabled_plugins 里,节点会直接起不来。部署顺序是:先启用插件 → 再写引用它的配置。启用检查:

    rabbitmq-plugins list -e | grep auth_backend_oauth2
  3. issuer 必须是 HTTPS。 用自签 CA 或私有 CA 签发的 IdP 证书时,必须通过 auth_oauth2.https.cacertfile 显式给 CA 证书,不会自动用系统信任库。

  4. 开了 OAuth 就默认关掉管理 UI 的 basic auth,但 HTTP API 是另一回事。 官方文档写得很清楚:management.oauth_enabled = true 之后,management.oauth_disable_basic_auth 默认就是 true——也就是管理 UI 只接受 OAuth 登录,要保留用户名密码表单必须显式设成 false。但 HTTP API 不受这个开关控制:OAuth 开启后 curl -u、rabbitmqadmin 这类 basic auth 依然可用,要禁掉得单独设 management.disable_basic_auth = true(此时除 GET/POST /definitions 要用查询串传 token 外,只剩 Bearer)。所以真正的可用性风险是管理 UI 这个入口在 IdP 故障时消失,而运维后路取决于你有没有另一条带 tag 的管理账号——判断能否接受这个耦合,或者至少留一个 internal 后端做后路(见第 9 节)。

  5. management.oauth_scopes 里必须有一个 tag scope。 官方文档要求至少包含 openid、profile 加一个 RabbitMQ scope,例如 rabbitmq.tag:management。少了 tag scope,浏览器登录会成功、回到管理 UI 后却是 Not authorized(见第 8 节)。

4. scope 怎么变成权限

翻译规则来自官方文档:scope 格式是 <permission>:<vhost_pattern>/<name_pattern>[/<routing_key_pattern>],<permission> 取 configure / read / write,后面三段都是支持 * 的通配符模式。

前缀规则是这套机制里最容易踩的坑,分两种:

  • 不配 scope_prefix(默认):前缀是 resource_server_id 加一个点。resource_server_id = my_rabbit 时,全 vhost 读权限的 scope 是 my_rabbit.read:*/*。
  • 配了 scope_prefix:整个前缀被替换,变成 <scope_prefix><permission>。设 scope_prefix = api://、权限 read:*/*,scope 就是 api://read:*/*。

「替换」而不是「追加」意味着:一旦设了 scope_prefix,你原来的 rabbitmq.tag:administrator 这类 scope 全部失效,得改成 <scope_prefix>tag:administrator。中途改这个值等于一次性收回所有权限,属于要发变更单的操作。

4.1 topic exchange 的绑定需要读和写,而且都是三段式

这是 AMQP 权限模型里最反直觉的一条,很多人配了 read:*/* + write:*/* 却卡在绑定上:

动作需要的 scope完整写法
绑定/解绑队列到 topic exchange队列+routing key 的 write,以及 exchange+routing key 的 readrabbitmq.write:*/*/* + rabbitmq.read:*/*/*
向 topic exchange 发布exchange+routing key 的 writerabbitmq.write:*/*/*
普通(非 topic)读写两段式足够rabbitmq.read:*/*、rabbitmq.write:*/*

三段式的第三段是 routing key pattern。要给应用开 topic 路由能力,就必须给 */*/* 形态的 scope,不能只给两段式。

4.2 通配符里的特殊字符要 URL 编码

*、%、/ 作为模式内的字面量出现时必须 URL 编码。这条规则平时用不到,一旦你的 vhost 或队列名里带这些字符就会踩到。

4.3 变量展开:一套 scope 服务所有用户

scope 模式里可以嵌 JWT claim,支持普通字符串 claim 加上 vhost 变量。官方教程里那个 client scope 就是这个用法:

rabbitmq.configure:*/q-{user_name}

含义是:允许这个用户配置以自己用户名开头的队列,且 vhost 不限。多租户场景下不用为每个用户生成一套 scope,一条规则就够,这也是上面配置里把 user_name 放在 preferred_username_claims.1 的原因——变量展开取的是 claim,claim 不存在时这条 scope 就永远匹配不上(又一个静默失败)。

5. 用 Keycloak 角色驱动权限(与 Kafka 的关键差异)

Kafka 内置的 OAUTHBEARER 只把 token 换成一个 principal,授权完全在 ACL,realm 角色变不成权限。RabbitMQ 这边不一样:scope 就是权限,所以只要让 scope 从角色推导出来就行。官方给了两条路。

路线一:scope_aliases 把角色名映射成 scope(推荐)。 token 里放的是 Keycloak 里好管理的角色名,RabbitMQ 侧翻译成真实 scope:

auth_oauth2.scope_aliases.admin     = rabbitmq.tag:administrator rabbitmq.read:*/* rabbitmq.write:*/* rabbitmq.configure:*/*
auth_oauth2.scope_aliases.developer = rabbitmq.tag:management rabbitmq.read:*/* rabbitmq.write:*/*

Keycloak 侧只要让 scope(或 additional_scopes_key 指向的 claim)里出现 admin 或 developer 就够了。这条路线把「Keycloak 侧发什么」和「RabbitMQ 侧要什么」解耦:以后调整权限粒度只改 RabbitMQ 配置,不用动 Keycloak 里几十个应用的 client scope。

scope_aliases 也支持表格形式(scope_aliases.1.alias / scope_aliases.1.scope),别名里带 :、/ 等字符时用这种写法更清楚。

路线二:直接从角色 claim 取 scope。 auth_oauth2.additional_scopes_key 接受空格分隔的多个 claim 路径,官方文档的示例就是三个路径一起写:

auth_oauth2.additional_scopes_key = extra_scope realm_access.roles resource_access.account.roles

官方教程的 realm 里就有一个 mapper 把 realm 角色写进 extra_scope claim(User Realm Role mapper,Claim name 填 extra_scope,Multivalued 打开),配合 additional_scopes_key = extra_scope 使用。

有一条官方警告必须记住:从 resource_access.account.roles 之类的路径取到的角色名不是 RabbitMQ 能识别的 scope,要靠 scope_aliases 映射才行。直接把 realm_access.roles 当 scope 用,结果是「认证通过、权限为空」。

两条路线的取舍:

做法优点风险
scope_aliases + 角色名权限口径集中在 RabbitMQ 配置里;Keycloak 侧只管角色别名一旦改名,所有依赖它的应用权限同时失效
additional_scopes_key 直取角色 claim不用在 RabbitMQ 里维护别名,Keycloak 即事实源角色名与 scope 语法耦合;account.roles 这类路径里的名字不是 scope,会静默无效

scope 的归属原则和别的系统一样:IdP 负责「你是谁、属于哪个组」,资源侧负责「这个组能做什么」。上面的映射表就是这条原则在 RabbitMQ 上的具体形态;更上层的分工讨论见 Keycloak 细粒度权限与授权策略。

6. 在 Kubernetes / Operator 上部署

Cluster Operator 下不需要挂自己的 rabbitmq.conf,两个字段就够:

apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
  name: rabbitmq
spec:
  replicas: 3
  rabbitmq:
    additionalPlugins:
      - rabbitmq_auth_backend_oauth2
    additionalConfig: |
      auth_backends.1 = oauth2
      auth_oauth2.resource_server_id = rabbitmq
      auth_oauth2.issuer = https://kc.example.com/realms/iam
      auth_oauth2.https.cacertfile = /etc/rabbitmq/certs/idp-ca.pem
      management.oauth_enabled = true
      management.oauth_client_id = rabbitmq-mgmt-ui
      management.oauth_scopes = openid profile rabbitmq.tag:management

三个 Operator 特有的点:

  • additionalPlugins 的变更不需要重启。 官方文档明确:改这个字段时 Operator 会在运行中的容器里执行 rabbitmq-plugins 启停插件,不重建 Pod;additionalConfig 的变更则要重启才生效。

  • 配置是从 additionalConfig 追加到 Operator 生成的 rabbitmq.conf 里的。 想看最终生效的文件:

    kubectl get -o yaml configmap rabbitmq-server-conf

    官方同时提醒:镜像本身还会往配置文件追加少量内容,所以某些键可能被覆盖——排查「我明明配了但没生效」时先看这个 configmap,而不是看你写的 YAML。

  • CA 证书要单独挂。 auth_oauth2.https.cacertfile 指向的是 Pod 内的路径,需要用 Secret 挂载到 override.statefulSet 的 volumes 里;写了一个不存在的路径,节点会因为读不到 CA 起不来。

7. 验证

四步,前一步不通过不要往下走。

# 1) 拿 token 并解码关键 claim —— 不要只看 HTTP 200
TOKEN=$(curl -s -X POST https://kc.example.com/realms/iam/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id=rabbitmq-app \
  -d client_secret="$KC_CLIENT_SECRET" | jq -r .access_token)

echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null \
  | jq '{iss, aud, scope, azp, exp}'

预期:iss 与 auth_oauth2.issuer 逐字符相同;aud 里含 rabbitmq;scope 里能看到你分配的那些 scope 名(这一段是排查权限问题的唯一权威依据)。

# 2) 确认节点能拉到 discovery 与 JWKS(注意是在 RabbitMQ 节点上执行,不是你的笔记本)
curl -sS https://kc.example.com/realms/iam/.well-known/openid-configuration | jq -r .jwks_uri
curl -sS "$(curl -sS https://kc.example.com/realms/iam/.well-known/openid-configuration | jq -r .jwks_uri)" \
  | jq '.keys[].kid'
# 3) 看 RabbitMQ 侧到底认出了什么权限
# 官方推荐的路径是解码浏览器里的凭据,而不是翻服务端日志:
# 管理 UI → 开发者工具 → Application → Storage → Local Storage → rabbitmq.credentials

管理 UI 登录后,浏览器把 token 存在 localStorage 的 rabbitmq.credentials 键里。把它复制到 jwt.io 解码,就能看到管理 UI 那次登录实际拿到的 scope——这是官方文档推荐的排查路径,比翻日志快。

# 4) 端到端跑一次真实收发,把认证与权限分开看
#    用户名位置随便填(被忽略),密码位置放 access_token
export AMQP_URL="amqps://ignored:${TOKEN}@rabbitmq.example.com:5671/%2f?cacertfile=/etc/rabbitmq/certs/idp-ca.pem"
python3 -c "
import os, pika
conn = pika.BlockingConnection(pika.URLParameters(os.environ['AMQP_URL']))
ch = conn.channel()
ch.queue_declare(queue='q-smoke', durable=True)
ch.basic_publish(exchange='', routing_key='q-smoke', body=b'ping')
print('publish ok')
"

第 4 步失败、第 1 步显示 scope 正常时,问题在权限模式与资源名的匹配上(vhost 名、队列名前缀、两段式 vs 三段式),不在认证上。

token 过期:长连接消费最容易忽略的一条

官方文档明确区分了两种协议的行为:

  • AMQP 1.0:连接上最新的 token 过期后,RabbitMQ 主动断开客户端。客户端要主动续期——官方 Java/.NET/Erlang 的 AMQP 1.0 客户端支持通过 HTTP-over-AMQP 1.0 向 /auth/tokens 发 PUT 来换新 token。
  • AMQP 0.9.1:token 过期后 Broker 不会断开连接,但过一段时间后拒绝后续操作。客户端可以用 update-secret 扩展方法换 token(Java 客户端文档有示例);客户端不支持 update-secret 时,只能断开重连。

生产含义很直接:长连接的消费者如果不在 token 生命周期内续期或重连,会先「静默变哑」,然后报权限错误——而不是干脆地掉线。Keycloak 默认的 token 生命周期比大多数消费者的运行时间短得多,所以「跑了一天一夜之后开始报 403 / 拒绝操作」这类现象,第一件事就是看客户端有没有做续期。这一点和 Keycloak 会话管理 里「服务账号的 token 生命周期与会话是两回事」是同一个认知。

8. 常见错误对照表

症状根因处理
管理 UI 打开只有一行 OAuth resource [rabbitmq] not available. OpenId Discovery endpoint https://<issuer>/.well-known/openid-configuration not reachableissuer 地址不可达、TLS 证书不被浏览器信任,或浏览器 CORS 拦截(IdP 未把管理 UI 的 origin 加进允许列表)看浏览器控制台的 net::ERR_*:ERR_CONNECTION_REFUSED 是网络,ERR_CERT_AUTHORITY_INVALID 是证书;都没有就搜 CORS,让 IdP 管理员加 origin
同一位置的报错结尾是 not compliant,控制台伴随 Missing jwks_uri / Missing token_endpointdiscovery 文档缺字段或 URL 写错(常见是漏了 /realms/<realm> 或错写成 not compliant 的旧路径)拿第 2.3 节的 curl 输出逐项比对,抄 issuer 原文
浏览器登录成功、回到管理 UI 显示 Not authorizedtoken 里没有管理插件 tag scope三种可能:management.oauth_scopes 没带 tag scope;client 上没把 rabbitmq.tag:management 设为 Default;开了 scope_prefix 导致实际 scope 名变了。用 localStorage 里 rabbitmq.credentials 解码确认
应用握手被拒,日志只有 invalid_token 之类,没有细节三类成因:签名/JWKS(含未知 kid)、时间窗口(exp)、aud 不含 resource_server_id先看第 7 节第 1 步的 token 解码:aud 不含 rabbitmq 就加 Audience mapper(included.custom.audience);iss 不对就修 issuer;都对则查 JWKS 可达性与 kid 刷新
日志出现 {bad_cert,hostname_check_failed}auth_oauth2.issuer 用的是通配符证书,CN/SAN 与 issuer 主机名不是逐字符相等(SaaS IdP 常见)排错文档给的修法是显式设 auth_oauth2.https.hostname_verification = wildcard。这个键的语义是「启用通配符感知的主机名校验」,配置表里默认值是 none——也就是按严格主机名匹配,通配符证书必然对不上。改 wildcard 等于放宽这一处校验,属于安全边界调整,写进变更单再改;多 IdP 场景要设到 auth_oauth2.oauth_providers.<name>.https.hostname_verification
客户端日志 frame length exceeded / frame is too large,AMQP 握手阶段 {frame_too_large,...}JWT 太长,超过初始帧上限。initial_frame_max 默认 4096 字节,而带大量 scope 的 token 很容易超首选精简 scope(去掉 RabbitMQ 用不到的);确实需要就调大 initial_frame_max(例如 8192)
认证通过,但所有操作都是权限不足token 的 scope 为空或前缀不匹配。默认前缀是 resource_server_id + ".",配了 scope_prefix 则整体替换解码 token 看 scope 原文,逐字符对齐配置里的前缀
绑定队列到 topic exchange 失败,读写本身正常topic 绑定需要队列侧 write 加上 exchange 侧 read,且都是三段式补 rabbitmq.read:*/*/* 与 rabbitmq.write:*/*/*
一次改名/升级后所有应用权限全空scope_prefix 被改(整体替换前缀)或 scope_aliases 的别名被改名两个键都属于「一改全失效」,改前先确认影响列表
跑一段时间后集中报错token 过期后未续期(AMQP 0.9.1 不断连但拒绝操作)客户端做 update-secret 或到期重连;核对 Keycloak 侧 token lifespan

9. 回滚

按影响面从小到大:

  1. 留一条 internal 后路。 auth_backends.1 = oauth2 后面再加 auth_backends.2 = internal,OAuth 认证失败时会回落到 RabbitMQ 本地用户库,IdP 挂掉时管理 UI 和运维入口还在。代价是要保留本地账号及其密码——这是一个安全折衷,属于临时止血手段,不是长期架构,用完必须删掉 .2 这一行和对应账号。
  2. 前端切换、不动数据面。 应用侧把 token 换回旧账号是纯客户端改动,RabbitMQ 侧不用动。前提是旧认证后端(internal 或 LDAP)在配置链里还保留着——和第 1 点配合使用。
  3. 管理 UI 先关 OAuth 再收尾。 management.oauth_enabled = false 就能让管理 UI 回到 basic auth 登录表单(oauth_disable_basic_auth 只在 OAuth 开启时才起作用,这里不用动它),不必删 Keycloak 侧任何东西。如果只是想「OAuth 和用户名密码并存」而不打算回滚,那就在开启 OAuth 时把 management.oauth_disable_basic_auth = false 一起写进配置。
  4. Keycloak 侧是增量对象,回滚成本低但要防误删。 client scope、Audience mapper、client 都是新增对象;删掉 Audience mapper 只会让新 token 失去 aud(RabbitMQ 随即拒绝),不影响其它接入方。删 client 之前确认没有别的消费方共用。
  5. 凭据与 CA 走轮换而非覆盖。 换 client secret 时先在 Keycloak 生成新值、验证可用,再停用旧值;CA 证书轮换要先把新旧 CA 都放进 cacertfile,等 token 都不再由旧 CA 签发后再移除。
  6. 不要在故障期间把 auth_oauth2.verify_aud 设成 false「先恢复再说」。 那是永久关闭 audience 校验,事后极容易忘记改回来;这类改动必须走变更单并写明失效时间。

常见问题(FAQ)

Keycloak 里的 realm 角色能直接当 RabbitMQ 的 vhost 权限用吗?

不能直接,但能映射,而且比 Kafka 容易得多。RabbitMQ 的权限就是 token 里的 scope,所以有两条路:用 auth_oauth2.scope_aliases.<角色名> = <scope 列表> 在 RabbitMQ 侧做翻译(推荐,权限口径集中在这里),或者用 additional_scopes_key 让 RabbitMQ 直接从角色 claim 里取 scope(此时角色名必须本身就是合法 RabbitMQ scope,否则会静默无效)。Kafka 内置实现没有这层能力,只能靠定制 principal builder 或发行版扩展。

为什么我配了 client_credentials 的 client,管理 UI 还是登不进去?

因为管理 UI 走的是浏览器授权码流程,需要一个 public client,而服务间认证的 client 是 confidential。官方文档要求 management.oauth_client_id 指向的 client 是 public web application,只有 UAM 这类 IdP 例外地需要 client secret。两个用途要建两个 client,这是设计使然,不是配置疏漏。

RabbitMQ 里已登录用户的权限能在线改吗?改 Keycloak 里的 client scope 多久生效?

权限随 token 走,所以已签发的 token 在有效期内仍然携带旧 scope,不会被追溯收回。要让改动生效,必须让客户端拿到新 token——也就是缩短 token 生命周期,或者主动重连。这一点和「撤销用户」的直觉不同:撤权是「下次认证开始生效」,不是「立即生效」。需要强一致撤权的场景,把 token lifespan 设短,或者用 additional_scopes_key 指向一个每次认证才计算的 claim。

要不要把 auth_oauth2.verify_aud 关掉来解决 audience 报错?

不要。官方文档明确写了 verify_aud = false 是「not recommended」的绕过手段。正确的修法是在 Keycloak 加 Audience mapper 让 aud 带上 resource_server_id。关掉校验等于让任何合法签发的 token 都能连你的集群——包括本该只给别的系统用的 token。

应用和运维都走同一套 OAuth,出故障时怎么还有入口?

两个不要省的东西:一是 auth_backends 链里保留 internal 后端作为临时后路(第 9 节),二是别把 management.oauth_disable_basic_auth 留在默认值上——它默认是 true,含义是管理 UI 只接受 OAuth,ID 提供方一挂,你连界面都进不去。注意这个开关只作用于管理 UI:HTTP API 上的 basic auth(curl -u、rabbitmqadmin)在 OAuth 开启后仍然可用,要单独用 management.disable_basic_auth = true 才会禁掉,所以它通常还能当第二条后路——但别把它当唯一后路,两条路都依赖同一个 internal 账号体系时,账号被清理掉就一起失效。

主要来源

  • RabbitMQ 官方文档(4.3): OAuth 2.0 Authentication Backend(auth_backends.1 = oauth2、resource_server_id 与 aud 校验、preferred_username_claims、additional_scopes_key 及其多 claim 路径写法、scope_prefix 的整体替换语义、scope_aliases、变量展开、scope-to-permission 翻译规则 configure|read|write:<vhost>/<name>[/<routingkey>]、topic exchange 绑定需要 read+write 的双三段式、通配符特殊字符需 URL 编码、管理 UI 十步配置与 management.oauth_scopes、management.oauth_disable_basic_auth 默认行为、token 过期与续期在 AMQP 0.9.1 / 1.0 上的差异、auth_oauth2.https.cacertfile、hostname_verification / verify_aud / require_exp 默认值表、多资源与多 IdP 配置)
  • Troubleshooting OAuth 2(frame_too_large 报错原文与 initial_frame_max 默认 4096、OAuth resource [rabbitmq] not available 与 not compliant 两类 discovery 报错及 net::ERR_* / CORS 定位路径、Not authorized 需要的四个 tag scope 与 localStorage rabbitmq.credentials 解码步骤、{bad_cert,hostname_check_failed} 与 hostname_verification = wildcard 修法)
  • Access Control(4.x 扁平 auth_backends.N 写法与别名表:oauth / oauth2 = rabbit_auth_backend_oauth2;auth_backends.1.authn / .authz 双段写法在 4.3 仍在文档中,用于认证与授权后端分离;后端链「第一个肯定结果即为最终结果」的语义)
  • Management Plugin(管理 UI 的 management.oauth_enabled / oauth_client_id / oauth_scopes 必填语义、oauth_disable_basic_auth 默认 true 表示 UI 只走 OAuth、与 HTTP API 侧独立的 management.disable_basic_auth(含 GET/POST /definitions 需查询串传 token 的例外)、管理 UI 作为 public app 不能存 client_secret、SP 发起登录与 IdP 发起登录两条路径)
  • rabbitmq-oauth2-tutorial:conf/keycloak/rabbitmq.conf(resource_server_id / issuer / additional_scopes_key 的官方示例形态)、conf/keycloak/import/test-realm.json(以 RabbitMQ scope 命名的 realm 级 client scope 全表与 include.in.token.scope、oidc-audience-mapper 的 included.custom.audience = rabbitmq、user_name / extra_scope mapper、后端 client 的 serviceAccountsEnabled 与管理 UI client 的 publicClient 差异、rabbitmq.configure:*/q-{user_name} 变量展开 scope)
  • Cluster Operator 文档: Custom Configuration(spec.rabbitmq.additionalConfig 追加语义与 configmap 核对命令、镜像追加内容可能覆盖配置的提示)、 Plugins(spec.rabbitmq.additionalPlugins,变更不需重启)
  • RabbitMQ Server 发布: v4.3.6(2026-09-14) 为本文版本基线