realm 是隔离边界
一句话总结
Realm 是容纳用户、客户端、角色和密钥的完整隔离单元。不同 Realm 之间互不知道对方的用户。
为什么需要了解这一点
第一次打开 Keycloak 时,会看到 master Realm。把应用用户创建在这里,是第一个常见错误。master 是用于管理 Keycloak 自身的 Realm,其中的用户会与 Keycloak 管理权限关联。应用必须单独创建 Realm。
Realm 是隔离单元,这句话有非常具体的含义:每个 Realm 都有不同的签名密钥、用户存储和令牌签发者(iss)。因此,Realm A 的令牌无法通过 Realm B 的 API 验证。多租户 SaaS 为每个租户设置 Realm 的设计正是由此而来。不过,Realm 达到数百个后,管理和内存负担都会增大,因此通常会在一个 Realm 内通过组和属性区分租户。
工作原理
客户端是在 Realm 内请求令牌的应用,分为两类。
公共客户端没有秘密。SPA 和移动应用属于这一类,必须启用 PKCE。准确注册重定向 URI 也很重要——若通配符范围过宽,就会形成窃取令牌的通道。
机密客户端拥有秘密。后端服务器属于这一类;启用服务账号后,可通过 client credentials 流程获取代表自身的令牌。
角色分为两个层次。Realm 角色在整个 Realm 中有意义(例如 admin),客户端角色只在特定客户端中有意义(例如 orders-api 的 refund)。服务数量较多时,使用客户端角色能够避免名称冲突。
组是用户的集合,可以映射角色。与其逐个给 300 名用户分配角色,不如把角色分配给组,再把用户加入组。组可以形成层级,子组会继承上级组的角色。
复合角色指一个角色包含其他角色。例如让 order-admin 包含 order-reader 后,就不必分别给管理员分配两个角色。
实际工作中的表现
令牌中角色所在的位置很容易混淆。Realm 角色放在 realm_access.roles,客户端角色放在 resource_access.<클라이언트ID>.roles。如果关闭客户端的“full scope allowed”,未映射到该客户端的角色会从令牌中消失。这可用于减小令牌并维持最小权限;但若不了解这一点就关闭,会出现“明明分配了角色,令牌里却看不到”的问题。
管理操作最好用 kcadm.sh 编写脚本。通过 Web 控制台手工创建的配置无法复现,预发布环境和生产环境会在不知不觉中变得不同。
验证令牌的一方必须检查什么
无论签发端配置得多好,如果接收端没有正确验证,就毫无意义。API Server 收到令牌后,要检查的内容是明确的。
- 签名——使用 Realm 公钥验证。密钥应从
/.well-known/openid-configuration指向的 JWKS 地址获取并缓存;收到未知密钥 ID 时重新获取。密钥会轮换,若只获取一次并永久使用,轮换当天所有验证都会失败。 - 签发者(
iss)——确认是否由我们的 Realm 签发。不检查它,其他 Realm 或其他 Keycloak 签发的令牌也可能通过。 - 受众(
aud)——确认令牌是否面向我们的服务。不检查它,为其他服务签发的令牌也能直接用于我们的 API。 - 过期时间(
exp)——只允许几十秒左右的时钟偏差。允许越宽松,被盗令牌的有效寿命就越长。
这里绝对不能让令牌决定使用哪种算法。 如果验证方直接相信头部中的 alg,攻击者就可能把它改成 none 或对称算法来绕过签名。应在代码中固定预期算法,若不同则立即拒绝。
令牌寿命也应一并设计。访问令牌较短(数分钟)、刷新令牌较长,是因为访问令牌无法被撤销。只要签名有效且尚未过期,即使在 Keycloak 中删除用户,该令牌仍然有效。因此,需要立即封禁时,要么依赖短寿命,要么改为每个请求都检查令牌状态;后者会让所有请求都经过 Keycloak,必须同时考虑性能和可用性。“已经退出登录,为什么还能使用?”这个问题的答案大多就在这里。
最后,还要决定验证失败时返回什么。签名或签发者错误应返回 401;令牌有效但该角色无权执行操作,则返回 403。若混为一谈,客户端无法区分应该重新登录还是应该申请权限,最终可能做出无限重复登录的页面。
下个练习将做什么
创建 Realm,分别创建公共客户端与机密客户端,再创建用户。之后在独立练习中创建角色和组,确认它们如何反映到令牌中,并尝试通过 scope 排除角色。