本文基于一个真实的菜单权限控制场景,梳理 Keycloak 中资源(Resource)、策略(Policy)、权限(Permission)三者的关系,并重点回答一个常见疑问:既然资源和权限已经够用,为什么还要有策略这一层?
适合在日后忘记细节时翻看复习。
一、背景:我的菜单权限场景
前端项目中有若干菜单,每个菜单根据用户权限决定是否显示。我原本的做法是:
-
每个路由菜单关联一个字段
menuPerms(权限标识符),例如menu:user:list、menu:order:view。 -
把这些
menuPerms全部导入 Keycloak 的客户端资源中。 -
希望登录时根据用户返回其有权访问的资源集合。
-
前端用这个资源集合去匹配菜单路由,决定渲染哪些菜单。
目标很明确:让 Keycloak 成为菜单权限的唯一数据源,前端只负责根据返回的资源集合过滤菜单。
二、核心三要素:资源、策略、权限
Keycloak 的授权服务由三个核心概念构成,它们是一条从「保护什么」到「根据什么规则允许」的递进链路。
1. 资源(Resource)
定义 :授权策略所要保护的对象。
-
在你的场景中,每一个
menuPerms标识符就是一个资源,例如menu:user:list。 -
资源是静态的、被动的,它只回答「有哪些东西需要被保护」。
-
资源可以定义作用域(Scope),例如
view、edit、delete,用来区分对同一资源的不同操作。
2. 策略(Policy)
定义 :授予访问权限必须满足的条件。
-
策略本身不关心具体的资源,只回答「满足什么条件的人可以访问」。
-
例如:基于角色的策略(用户拥有
admin角色)、基于用户的策略(用户是张三)、基于组的策略、基于时间的策略等。 -
策略是可复用的,一个策略可以应用到多个权限中。
3. 权限(Permission)
定义 :连接资源 与策略 的桥梁 ,回答「谁 (策略)能对什么 (资源)执行什么操作」。
-
创建权限时,需要指定:保护哪个资源、应用哪些策略、允许哪些作用域。
-
权限是最终做出「授予」或「拒绝」裁决的地方。
-
只有当权限所关联的策略评估通过时,该资源对该用户的访问才会被允许。
三者的关系图
text
资源 (Resource) 权限 (Permission) 策略 (Policy)
保护对象 ←──── 桥梁/裁决点 ────→ 判断条件
menu:user:list 权限A 角色策略:admin
menu:order:view ├─ 保护 menu:user:list 角色策略:财务
└─ 引用 角色策略:admin
一句话概括:权限 = 资源 + 策略。资源是「保护什么」,策略是「凭什么」,权限是「把两者绑在一起并做出裁决」。
三、我的疑问:既然资源和权限够用,为什么还要策略?
这是最关键的疑问。如果只是「角色 → 资源」这种简单映射,看起来只需要:
-
创建资源
menu:user:list -
创建权限,保护该资源,直接绑定角色
admin
这样就变成了「当前角色拥有当前资源」,策略层似乎完全多余。
为什么 Keycloak 还要在中间加一层策略?
四、策略层存在的意义
1. 复用:一处定义,多处引用
假设没有策略层,权限直接绑角色:
-
权限 A 保护资源 1,绑定角色
admin -
权限 B 保护资源 2,绑定角色
admin -
权限 C 保护资源 3,绑定角色
admin
当规则变化,比如「admin 还必须属于财务部才能访问」时,你必须在每一个权限里逐个修改判断条件。权限一多,维护成本爆炸。
有了策略层:
-
创建一个策略「admin 且属于财务部」
-
权限 A/B/C 都引用这一个策略
规则变化时,只改策略一处,所有引用它的权限自动生效。
2. 组合:支持多维判断
Keycloak 的策略支持逻辑组合:
-
Aggregate Policy(聚合策略):把多个策略用 AND/OR 组合。例如「(是 admin) OR (是财务 AND 已认证)」。
-
策略可以嵌套引用策略。
没有策略层,判断维度只能是「角色」这一个。有了策略层,可以自由组合:角色 + 用户属性 + 时间 + 客户端 + 上下文。
3. 多种策略类型:不止是角色
Keycloak 内置的策略类型远不止角色:
| 策略类型 | 判断依据 |
|---|---|
| Role Policy | 用户是否拥有某角色 |
| User Policy | 是否是特定用户 |
| Group Policy | 用户是否在某个组 |
| Client Policy | 请求来自哪个客户端 |
| Time Policy | 当前时间是否在范围内 |
| Aggregated Policy | 上述策略的逻辑组合 |
| JS Policy | 自定义 JavaScript 逻辑 |
| Regex Policy | 基于声明的正则匹配 |
没有策略层,这些判断逻辑只能硬编码进权限,或者根本无法表达。有了策略层,可以写出「只有来自移动端 App、且用户属于 VIP 组、且在工作时间内」才能访问某资源这样的规则。
4. 关注点分离:决策与绑定解耦
-
权限回答:「哪个资源,允许哪些操作,由哪些策略来裁决」
-
策略回答:「满足什么条件才算通过」
这两件事的变化频率和变化原因不同:
-
资源/权限的调整通常跟业务对象变化有关。
-
策略的调整通常跟安全规则变化有关。
分开后,改安全规则不会动到资源绑定关系,改资源绑定也不会动到安全规则。
5. 支持更细粒度的决策:为 ABAC 铺路
策略评估的结果不只是「通过/不通过」,还可以携带信息,供权限层做更复杂的裁决(多个策略满足任意一个即可,或必须全部满足)。这为 ABAC(基于属性的访问控制) 提供了基础,而纯角色绑定只能做 RBAC(基于角色的访问控制)。
6. 可审计、可解释
当需要回答「这个用户为什么能看这个菜单」时,策略层让决策链路清晰可查:哪个权限、引用了哪些策略、每个策略评估结果如何。纯绑定角色则无法提供这种解释能力。
五、正确实现路径(结合我的菜单场景)
我原本的思路是「给每个用户分配这些策略」,但正确做法是给权限分配策略,而不是直接给用户分配策略。完整路径如下:
步骤 1:将菜单权限标识符创建为资源
把每个 menuPerms(如 menu:user:list)导入 Keycloak 的客户端资源中。
步骤 2:创建可复用的策略
根据业务规则创建策略。例如:
-
角色策略「拥有
menu:view角色」 -
组策略「属于财务组」
步骤 3:创建权限并关联资源与策略
为菜单资源创建基于资源的权限:
-
选择要保护的菜单资源(如
menu:user:list) -
关联上创建好的策略
这样,任何满足策略条件的用户,在请求这个菜单资源时,权限都会评估通过。
步骤 4:登录时获取用户有权访问的资源集合
用户登录时,前端应用调用 Keycloak 的 Entitlement API (权利端点)或发送特定的令牌请求,获取该用户在当前客户端下所有被授予权限的资源列表。
-
接口示例:
GET /authz/entitlement/{client_id} -
返回的 RPT(请求方令牌)中会包含授权信息。
步骤 5:用返回的资源集合过滤菜单
Keycloak 返回的响应中包含用户有权访问的资源名称(即 menuPerms 标识符)。前端拿到资源集合后,去匹配预先定义好的菜单路由,只渲染匹配成功的菜单项。
六、关键实现提示与坑
1. 使用 Entitlement API
获取用户资源权利的接口是 Entitlement API,返回的 RPT 中包含授权信息。这是实现动态菜单的关键。
2. 谨慎对待默认配置
Keycloak 会为新的资源服务器创建一个默认资源 (如 /*)和默认策略 (总是授予)。这个默认策略会允许访问任何资源,务必在自定义配置前删除或修改它,否则你的权限配置将失效。
3. 渐进式采用策略
如果场景目前确实很简单(「某角色能看到某菜单」),用「权限直接绑角色」也能跑通。可以先用资源和权限跑通,等规则复杂了再引入策略。这是完全合理的渐进式做法。
七、一句话总结
资源 是保护对象,策略 是判断条件,权限是把两者绑在一起并做出裁决的桥梁。
策略层的本质,是把「判断谁能访问」的逻辑从「资源与角色的绑定关系」中抽离出来,使其成为可复用、可组合、可动态评估的独立单元。简单场景可以不用它,但一旦访问控制规则变复杂,没有策略层就会导致规则散落在成百上千个权限配置里,无法维护。
Keycloak 的设计不是「为了多一层而多一层」,而是为了支撑从简单 RBAC 到复杂 ABAC 的平滑演进。
八、速查表
| 概念 | 回答的问题 | 我的场景中的对应物 |
|---|---|---|
| 资源 Resource | 保护什么? | 每个 menuPerms 标识符 |
| 策略 Policy | 凭什么条件允许? | 角色策略、组策略等 |
| 权限 Permission | 谁能在什么资源上做什么? | 保护某菜单资源 + 引用某策略 |
| Entitlement API | 用户有权访问哪些资源? | 登录时调用,返回资源集合 |
| 默认策略 | 兜底规则(危险) | 必须删除或修改,否则权限失效 |