RabbitMQ 接入 Keycloak OAuth 2.0:IAM 权限映射与排错 | IDaaS Book
场景
- 安全评审要求消息中间件不再维护独立账号库,应用和运维人员统一走 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)。
这个设计带来两个后果,也是本文所有排错的出发点:
- 权限不落在 RabbitMQ 的数据库里,而是随 token 走。 撤权 = 让下一个 token 不再带那个 scope,不需要在 RabbitMQ 侧改任何东西;反过来,已经签发的 token 在有效期内依然是有效的,
user表里也看不到任何痕迹。 - 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 的scopeclaim。
2.1 管理 UI 和后端应用要用两个不同的 client
这是最容易混的一处,官方文档写得很直白但很容易被忽略:
| 用途 | client 类型 | 需要的开关 |
|---|---|---|
应用连 AMQP / HTTP API(client_credentials) | confidential | Client authentication = On、Service accounts roles = On;Standard flow / Implicit flow / Direct access grants 全部关闭 |
| 运维用浏览器登录管理 UI(SP 发起登录) | public | Client 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 点五处容易配错的地方:
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——它确实能同时承担认证与授权,按页面分类去理解会以为它做不了认证。插件必须先启用。 配置里引用了
oauth2后端,而rabbitmq_auth_backend_oauth2没有出现在enabled_plugins里,节点会直接起不来。部署顺序是:先启用插件 → 再写引用它的配置。启用检查:rabbitmq-plugins list -e | grep auth_backend_oauth2issuer必须是 HTTPS。 用自签 CA 或私有 CA 签发的 IdP 证书时,必须通过auth_oauth2.https.cacertfile显式给 CA 证书,不会自动用系统信任库。开了 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 节)。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 的 read | rabbitmq.write:*/*/* + rabbitmq.read:*/*/* |
| 向 topic exchange 发布 | exchange+routing key 的 write | rabbitmq.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 reachable | issuer 地址不可达、TLS 证书不被浏览器信任,或浏览器 CORS 拦截(IdP 未把管理 UI 的 origin 加进允许列表) | 看浏览器控制台的 net::ERR_*:ERR_CONNECTION_REFUSED 是网络,ERR_CERT_AUTHORITY_INVALID 是证书;都没有就搜 CORS,让 IdP 管理员加 origin |
同一位置的报错结尾是 not compliant,控制台伴随 Missing jwks_uri / Missing token_endpoint | discovery 文档缺字段或 URL 写错(常见是漏了 /realms/<realm> 或错写成 not compliant 的旧路径) | 拿第 2.3 节的 curl 输出逐项比对,抄 issuer 原文 |
浏览器登录成功、回到管理 UI 显示 Not authorized | token 里没有管理插件 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. 回滚
按影响面从小到大:
- 留一条
internal后路。auth_backends.1 = oauth2后面再加auth_backends.2 = internal,OAuth 认证失败时会回落到 RabbitMQ 本地用户库,IdP 挂掉时管理 UI 和运维入口还在。代价是要保留本地账号及其密码——这是一个安全折衷,属于临时止血手段,不是长期架构,用完必须删掉.2这一行和对应账号。 - 前端切换、不动数据面。 应用侧把 token 换回旧账号是纯客户端改动,RabbitMQ 侧不用动。前提是旧认证后端(
internal或 LDAP)在配置链里还保留着——和第 1 点配合使用。 - 管理 UI 先关 OAuth 再收尾。
management.oauth_enabled = false就能让管理 UI 回到 basic auth 登录表单(oauth_disable_basic_auth只在 OAuth 开启时才起作用,这里不用动它),不必删 Keycloak 侧任何东西。如果只是想「OAuth 和用户名密码并存」而不打算回滚,那就在开启 OAuth 时把management.oauth_disable_basic_auth = false一起写进配置。 - Keycloak 侧是增量对象,回滚成本低但要防误删。 client scope、Audience mapper、client 都是新增对象;删掉 Audience mapper 只会让新 token 失去
aud(RabbitMQ 随即拒绝),不影响其它接入方。删 client 之前确认没有别的消费方共用。 - 凭据与 CA 走轮换而非覆盖。 换 client secret 时先在 Keycloak 生成新值、验证可用,再停用旧值;CA 证书轮换要先把新旧 CA 都放进
cacertfile,等 token 都不再由旧 CA 签发后再移除。 - 不要在故障期间把
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 与 localStoragerabbitmq.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_scopemapper、后端 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) 为本文版本基线