第 10 章拆解 — 动态权限与缓存:Casbin、五种缓存形态与失效的艺术
动态权限与缓存:Casbin、五种缓存形态与失效的艺术
这一章由两个松散耦合的主题拼成(书里自己承认):前半把权限从代码里解放出来—— 管理员在线改策略,不再找开发发版;后半把缓存从「加个 Redis」讲成一门完整学问—— 淘汰策略、五种形态、三个操作,以及那个反复被引用的行业教训: 缓存唯一的难事是知道什么时候该让它失效。 主线例子:管理员把「贡献者」角色 的某条权限在线改掉的全程,以及这次改动撞上了哪几道缓存防线。
1. 顶层全景
权限建模:ACL(谁/哪/什么)vs RBAC(+怎么/何时,角色可继承)
↓
用户组三角色:Contributor / Manager(继承 Contributor)/ Administrator
↓
动态化:策略搬进可写存储(文件 → 数据库)
↓
Casbin:PERM 元模型统一 ACL 与 RBAC;model.conf + policies.csv + 适配器
↓
平台管理:策略入库,UI 在线改,下次请求即生效
──────── 第二个主题 ────────
缓存:Wilkes 1965 的快慢内存并用思想
→ 四种淘汰策略 → 五种形态(应用内/分布式/全局/CDN/代码级)
→ 三操作(存/取/失效)+ guard 防并发重算
→ 授权 × 缓存:头号候选;策略变更必失效缓存
图说:一条「改权限」的线,穿过两个主题的全部机制。2. 角色管理回顾:ACL 与 RBAC 各回答几个问题
ACL(访问控制列表)= 一张「谁-哪-什么」的表。 基本形态是 「实体-资源-操作」三元组(某用户被允许对某文件做读操作); 用户一多模式就重复,于是抽组(GBAC)——组有预定义权利,成员直接继承 (超级用户能改系统文件,普通用户不行)。ACL 回答三个问题:Who(谁有权)、 Where(哪些资源)、What(什么操作)1。
RBAC 跳过「给人配权利」,直接从角色出发。 角色不是一群人,而是 一组职责(系统管理员的定义是他「管其他用户」这件事,不是成员名单); 角色可以分层继承。比 ACL 还多答两问:How(允许怎么改—— 「可写」允许清空 CSV,「怎么定义」能精确到哪些列、什么值)、 When(权利可绑会话,过时即失效,再用要重新授权)2。
两者重叠很大,「只要概念清楚,任一都能实现同样的特性」3。
3. 三个用户组的权利清单
把第 2 章立的角色落成权利表(这是后面 Casbin 策略的素材):4
| 角色 | 权利 | 继承关系 |
|---|---|---|
| Contributor | 建票、更新票状态、加评论 | 基础层 |
| Manager | 建项目、关项目 | 继承 Contributor 的全部,外加自己的 |
| Administrator | 建用户、停用户、改用户角色 | 不继承任何人(职责无重叠;权限最大的角色,指派要最谨慎,最好限定会话) |
书里点了一句实话:这种逐端点手写检查的复杂度高到「最好外包给久经测试的库」—— 于是 Casbin 登场5。
动态角色管理 = 把关联关系存进可写的媒介(文件/数据库),能在线改。 好处是把「管权限」的职责还给管理员,不消耗开发团队的发版。 配套贴士:allow-all(默认全放行)还是 deny-all(默认全拒)按区域选—— deny-all 更安全(不会意外放行)但没配角色前没法测;面向公众的页面默认公开, 内部区域默认拒绝6。
4. Casbin:一个库统一 ACL 和 RBAC
Casbin 是什么: Go 写的授权库(多语言绑定,Rust 是其中之一), 提供基于策略的声明式授权,在拦截点调用。它的词典三个词: Subject(访问者的用户)、Object(被访问的资源)、Action(对资源的操作)。 它接管四件事:策略执行(定义完帮你验)、存储(各种适配器)、 角色映射管理(用户-角色、角色-角色,可建复杂层级)、内置超级用户; 最强的是自定义匹配语言(几乎无限的灵活性)。 它故意不做的两件:认证(验凭据不在其列)、用户/角色的增删查改界面。 它也不是密码容器——它是认证之后的第二道层7。
两个配置件: Model(策略的「写法」,ACL、RBAC 等)+ Adapter
(策略的「存处」,默认文件,可换数据库)。Enforcer::new(model.conf, policy.csv)
一行建好执行器,异步、初始化失败会抛错;校验就一句
e.enforce(("manager", "ticket", "update-state")).await?8。
4.1 PERM 元模型:四段配置撑起一切
Casbin 的灵活性来自 PERM 元模型——在 ACL/RBAC 之上再抽象一层, 甚至能混合两种方法9:
| 段 | 干什么 | 例子 |
|---|---|---|
| Request definition | 请求长什么样 | r = sub, obj, act |
| Policy definition | 策略每行的字段与顺序 | p = sub, obj, act |
| Policy effect | 多条匹配结果怎么合流 | e = some(where (p.eft == allow)) |
| Matchers | 请求与策略怎么算匹配 | m = r.sub == p.sub && r.obj == p.obj && r.act == p.act |
ACL 的两个文件: model.conf 如上四段;policies.csv 一行一个场景
(p, alice, data1, read——alice 可读 data1;没写的全部不允许)10。
升级成 RBAC 只差两步: ① model.conf 加一段 g = _, _
(两个下划线 = 支持两级继承;小写 g 不是某个组,而是一套角色体系,
可以并存多套互不干扰);② matcher 换成 g(r.sub, p.sub) && …
(请求者要「扮演」策略里的角色);③ policies.csv 用 g 行写继承
(g, manager, contributor——manager 继承 contributor;
传递性无限深度)11。
一条红字注释: 策略里的名字是任意字符串,Casbin 不校验拼写和重名—— 「用户角色绝不能和资源角色同名,否则会互相干扰」12。
5. 平台管理:策略入库,在线生效
文件存储的死穴:每次改策略都要重新部署文件。解法就是第 2 章说过的 「入库即成动态」——Casbin 有现成的 SQLx 适配器和 SeaORM 适配器。 换适配器后初始化方式变了(先造 adapter 实例再传给 enforcer),其余照旧; 数据库里改一条策略,下一次授权请求就按新的来——这正是平台管理要的效果: 管理员在 UI 上动权限,无需开发、无需 SRE13。
6. 缓存:从 1965 年讲起
思想出处: 英国计算机科学家 Maurice Wilkes 1965 年的论文: 快而贵的内存当慢而大的内存的临时容器——数据平时住慢存, 被用过就抄一份进快存,下次从快存取;快存满了就要挑谁踢出去。 不是镜像全部数据,而是两种内存并用,各取所长14。
6.1 四种淘汰(eviction)策略
淘汰 = 缓存满了腾位置时按什么规矩删旧条目(用户不操心,引擎自动做):15
| 策略 | 踢谁 | 长处 | 短处 |
|---|---|---|---|
| LRU(最近最少用) | 最久没被访问的 | 简单、效率高、适应性强 | 要记访问时间戳(内存开销)、冷启动慢、顺序不代表重要性 |
| LFU(最不常用) | 访问次数最少的 | 长期模式优、计数器开销低 | 冷启动慢、模式一变就不灵、计数器要线程安全 |
| FIFO(先进先出) | 最老的 | 简单、行为可预测、省内存 | 不问重要性、适用面窄、同样要预热 |
| 随机 | 随机 | 极简、无偏、开销最低 | 命中率可能很差 |
「适合这个场景的不一定适合那个」——按属性选,别按流行选16。
6.2 五种缓存形态(在哪一层挡)
- 应用服务器缓存: 同一请求的响应直接命中;多实例 + 负载均衡时, 请求被分到没缓存的那台就重算——引出下面两种。
- 分布式缓存: 缓存分片挂在实例上,用哈希函数从请求算出该去哪片取—— 任何实例都知道去哪拿。
- 全局缓存: 一个中央缓存当数据库唯一门户,未命中由它回源、灌水; 和垂直切片架构(每组件自有库)不兼容,设计基建时就要想好。
- CDN: 静态资产全球分发、就近服务;只送内容、不算内容—— 你的应用不会跑在 CDN 上,这是故意的。
- 代码级缓存(细活): 库里直接调函数,粒度自己控、过期策略自己定, 代价是代码里全是缓存行——适合把单个高成本计算的结果存下来。17
提供者: 云厂商各家有方案;内存库里 Redis(本职就是缓存的常见化身)、 Memcached、Hazelcast;代码级 Rust 三选一——Moka(功能全, 并发同步/异步都有,TinyLFU 近似最优命中)、Mini Moka(精简版,无后台线程)、 Quick Cache(轻量高性能,CLOCK-Pro)。18
全书的缓存纪律(原话口径): 「先不带缓存实现,确认瓶颈之后再加缓存。」19
6.3 三个操作与 guard
存: 最朴素 cache.insert("square", "blue");更常见的是「取不到就灌」——
Quick Cache 的 get_or_insert_with(key, 闭包):闭包在缺失时被调、
结果立刻进缓存。短处:并发访问同一 key 时,多个线程会重复执行那个昂贵闭包。20
防重复的 guard: get_value_or_guard_async——一个 key 一旦被发起计算,
其他访问者拿到的是「占位守卫」,要等值备好或守卫被丢弃;
「保证昂贵的值只在首次访问时生产一次」。代价:手动检查、手动 insert。21
失效(invalidation)按范围分两档: remove(删单个——某 key 的语义没了,
下次访问强制重算);clear(全清——改权限时最安全:把所有权限缓存冲掉,
随访问逐渐重新灌水)22。
7. 授权 × 缓存:为什么这两个主题在一章
因为授权是全应用被调用最频繁的功能之一——每个操作的第一步都是「这事合法吗」。 把策略数据放进内存(或 Redis)快取,请求才不必在授权上耗费跟响应本体 相当的时间。 但授权有自己的生命周期,缓存必须配合:23
- 定义策略时要存得可取(deny-all/allow-all 混合也行);
- 校验请求时按请求里的 key 一次调用取出相关策略;
- 策略变更时必须让缓存失效——否则按过期策略放行,等于门户大开。
Casbin 对缓存的态度(原话):无所谓——文件缓存、数据库缓存都行, 它只要求数据在需要时可得;所以缓存要我们自己加。 Casbin 也支持适配器之间迁移、或自写适配器24。
8. 主线例子走一遍:一次在线改权限
管理员在平台 UI 上把「Contributor 不能改票优先级」改成「可以」
│ ① UI 写库:策略表多一行(入库后下次 enforce 即生效——Casbin 动态适配器)
▼
同一时刻,某请求的授权检查命中了应用内的策略缓存 —— 旧答案!
│ ② 这正是「策略变更必须失效缓存」要防的事故
▼
实现选择 A:改策略的 handler 顺手 remove(改的那个 key)
B:clear 全清(权限这种相互关联的,书里推荐——
「冲掉所有权限缓存,随访问逐渐重灌,最安全」)
│ ③ 清完的第一个请求回源 Casbin(库里的新策略)→ 写缓存 → 后续全快
▼
若无缓存的授权本来就工作正常 —— 缓存只是把它变快;
「授权无缓存也必须正确,缓存是性能优化不是正确性补丁」
图说:改权限这条线,依次穿过 UI、数据库、Casbin、缓存失效、回源重灌五步。
(权限条目为演示编的;remove/clear 语义出自原文。)9. 作者的判断与证据(分开标)
有出处的: Wilkes 1965 的缓存思想、四种淘汰策略的优缺点表、 Moka/MiniMoka/QuickCache 特性对比、guard 的语义、clear 用于权限变更的建议, 均为原书原文1415182122。
作者自己的评估:
- 「先无缓存、确认瓶颈再加」的经验法则19;
- 「权限相互关联,变更用 clear 最安全」是稳妥取向(细粒度失效在高频变更下更省,但风险高);
- 「本章两主题松散耦合,当参考手册用」是作者自己的定位25。
10. 边界与局限
- 缓存一致性(分布式)没展开: 多实例下 remove/clear 怎么广播,书里留给读者;
- Casbin 的性能与加载策略(启动全量加载 vs 按需查询)未比较;
- CDN 只在概念层——缓存键设计、失效推送(purge)这些实操没碰;
- 「管理员角色不继承」是教学设定——脚注自己承认实践中管理员常需要 contributor 能力26。
11. 可带走的
主线例子一行走完: UI 改策略 → 入库 → 缓存失效(clear)→ 回源 → 重灌 → 后续命中。
- ACL 答 Who/Where/What;RBAC 再加 How/When,角色按职责定义、可继承。
- 动态权限 = 策略进可写存储; 管理员的战场在 UI,不在代码。
- Casbin 三词:Subject/Object/Action;四件接管、两件不管(认证、用户管理)。
- PERM 元模型四段:请求、策略、效果、匹配; RBAC = ACL +
g = _, _+ 角色匹配器。 - 策略名字是裸字符串——用户角色与资源角色严禁同名。
- 缓存四策略按场景选; 五种形态对应架构里不同的层。
- 先无缓存跑通,确认瓶颈再上缓存——这是全书的纪律。
- guard 防并发重复计算昂贵的值;
get_or_insert_with简单但可能重复。 - 失效两档:remove 单个、clear 全体; 权限这类相互关联的数据,变更用 clear 最稳。
- 授权是缓存的头号候选;但「无缓存也必须正确」——缓存只买速度,不买正确性。
12. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| ACL 三问与 GBAC | Administration, Dynamic ACLs, and Caching | text/81-fm-introduction.txt:74(搜「Who: The entity」)、:84(搜「GBAC」) |
| RBAC 的 How/When | 同上 | text/81-fm-introduction.txt:100(搜「How」)、:103(搜「When」) |
| 三角色权利表 | 同上 | text/81-fm-introduction.txt:130(搜「Contributor」)、:144(搜「Administrator」) |
| 「外包给测试过的库」 | 同上 | text/81-fm-introduction.txt:152(搜「outsource」) |
| allow-all/deny-all 按区域 | 同上 | text/81-fm-introduction.txt:168(搜「deny-all」) |
| Casbin 三词与四件事 | 同上 | text/81-fm-introduction.txt:192(搜「Subject」)、:208(搜「Policy enforcement」)、:230(搜「Authentication」) |
| 两个配置件与 enforce | 同上 | text/81-fm-introduction.txt:271(搜「Model」)、:308(搜「enforce」) |
| PERM 四段 | 同上 | text/81-fm-introduction.txt:341(搜「Matcher」)、:344(搜「Effect」) |
| ACL 两文件 | 同上 | text/81-fm-introduction.txt:354(搜「request_definition」)、:403(搜「alice, data1」) |
| RBAC 三步与传递性 | 同上 | text/81-fm-introduction.txt:422(搜「role_definition」)、:483(搜「g(r.sub」)、:1513(搜「transitivity」) |
| 同名干扰警告 | 同上 | text/81-fm-introduction.txt:499(搜「interfere」) |
| 适配器与在线生效 | 同上 | text/81-fm-introduction.txt:522(搜「SeaORM」)、:551(搜「next authorization attempt」) |
| Wilkes 1965 | 同上 | text/81-fm-introduction.txt:560(搜「Maurice Wilkes」) |
| 四种淘汰策略 | 同上 | text/81-fm-introduction.txt:575(搜「Least recently used」)、:593(搜「First-in-first-out」)、:602(搜「Random replacement」) |
| 五种缓存形态 | 同上 | text/81-fm-introduction.txt:629(搜「Application server cache」)、:638(搜「hashing function」)、:644(搜「vertical slices」)、:650(搜「nearest possible」)、:718(搜「granular control」) |
| 代码级三库对比 | 同上 | text/81-fm-introduction.txt:722(搜「Moka」)、:1044(搜「Comparison」) |
| 先无缓存的纪律 | 同上 | text/81-fm-introduction.txt:1155(搜「without caching first」) |
| get_or_insert_with 与 guard | 同上 | text/81-fm-introduction.txt:1199(搜「get_or_insert_with」)、:1229(搜「get_value_or_guard_async」) |
| remove/clear 与权限清缓存 | 同上 | text/81-fm-introduction.txt:1254(搜「remove」)、:1266(搜「flush all privileges」) |
| 授权 × 缓存生命周期 | 同上 | text/81-fm-introduction.txt:1272(搜「most frequently accessed」)、:1285(搜「invalidate」) |
| 「无缓存也必须正确」 | 同上 | text/81-fm-introduction.txt:1295(搜「flawlessly even without caching」) |
Footnotes
-
出处:「Access control lists」(
text/81-fm-introduction.txt:74,搜「Who: The entity」;:84,搜「GBAC」)。 ↩ -
出处:「Role-based access control」(
text/81-fm-introduction.txt:93,搜「hierarchy」)、:100(搜「How: The alteration」)、:103(搜「When: The access rights」)。 ↩ -
出处:「Role-based access control」(
text/81-fm-introduction.txt:117,搜「considerable overlap」)。 ↩ -
出处:「User groups」(
text/81-fm-introduction.txt:130,搜「Create ticket」)、:137(搜「Create project」)、:144(搜「Create user」)。 ↩ -
出处:「User groups」(
text/81-fm-introduction.txt:152,搜「outsource this responsibility」)。 ↩ -
出处:「Dynamic role management」(
text/81-fm-introduction.txt:158,搜「accessible medium」)、:168(搜「allow-all or deny-all」)。 ↩ -
出处:「Overview of Casbin」(
text/81-fm-introduction.txt:192,搜「Subject」)、:204(搜「Policy enforcement」)、:230(搜「out of scope」)、:244(搜「second layer」)。 ↩ -
出处:「Getting started with Casbin」(
text/81-fm-introduction.txt:284,搜「Enforcer::new」)、:308(搜「enforce」)。 ↩ -
出处:「Modeling with Casbin」(
text/81-fm-introduction.txt:332,搜「PERM」)。 ↩ -
出处:「Modeling with Casbin」(
text/81-fm-introduction.txt:354,搜「request_definition」)、:403(搜「alice, data1, read」)、:413(搜「No other options are allowed」)。 ↩ -
出处:「Modeling with Casbin」(
text/81-fm-introduction.txt:422,搜「role_definition」)、:483(搜「g(r.sub」)、:1513(搜「transitivity」)。 ↩ -
出处:「Modeling with Casbin」(
text/81-fm-introduction.txt:499,搜「interfere」)。 ↩ -
出处:「Platform administration」(
text/81-fm-introduction.txt:522,搜「SeaORM」)、:551(搜「next authorization attempt」)。 ↩ -
出处:「Caching」(
text/81-fm-introduction.txt:560,搜「Maurice Wilkes」)。 ↩ ↩2 -
出处:「Cache eviction strategies」(
text/81-fm-introduction.txt:575,搜「Least recently used」)、:584(搜「Least frequently used」)、:593(搜「First-in-first-out」)、:602(搜「Random replacement」)。 ↩ ↩2 -
出处:「Cache eviction strategies」(
text/81-fm-introduction.txt:611,搜「best fits our purposes」)。 ↩ -
出处:「Types of caching」(
text/81-fm-introduction.txt:632,搜「load balancer」)、:638(搜「hashing function」)、:644(搜「vertical slices」)、:650(搜「nearest possible physical location」)、:718(搜「granular control」)。 ↩ -
出处:「Code level caches」(
text/81-fm-introduction.txt:722,搜「Moka」)、:728(搜「Quick Cache」)、:1044(搜「Comparison of three」)。 ↩ ↩2 -
出处:「Code level caches」(
text/81-fm-introduction.txt:1155,搜「without caching first」)。 ↩ ↩2 -
出处:「Retrieving data from the cache」(
text/81-fm-introduction.txt:1199,搜「get_or_insert_with」)、:1220(搜「more than one」)。 ↩ -
出处:「Retrieving data from the cache」(
text/81-fm-introduction.txt:1229,搜「get_value_or_guard_async」)、:1223(搜「produced only once」)。 ↩ ↩2 -
出处:「Invalidating the cache」(
text/81-fm-introduction.txt:1254,搜「Remove a single entry」)、:1266(搜「flush all privileges」)。 ↩ ↩2 -
出处:「Caching and authorization」(
text/81-fm-introduction.txt:1272,搜「most frequently accessed」)、:1285(搜「invalidate that cache」)。 ↩ -
出处:「Caching and authorization」(
text/81-fm-introduction.txt:1289,搜「indifferent」)。 ↩ -
出处:「Conclusion」(
text/81-fm-introduction.txt:1322,搜「loosely coupled」)。 ↩ -
出处:脚注 1(
text/81-fm-introduction.txt:1489,搜「In practice」)。 ↩