编程语言研究书库

第 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 五种缓存形态(在哪一层挡)

  1. 应用服务器缓存: 同一请求的响应直接命中;多实例 + 负载均衡时, 请求被分到没缓存的那台就重算——引出下面两种。
  2. 分布式缓存: 缓存分片挂在实例上,用哈希函数从请求算出该去哪片取—— 任何实例都知道去哪拿。
  3. 全局缓存: 一个中央缓存当数据库唯一门户,未命中由它回源、灌水; 和垂直切片架构(每组件自有库)不兼容,设计基建时就要想好。
  4. CDN: 静态资产全球分发、就近服务;只送内容、不算内容—— 你的应用不会跑在 CDN 上,这是故意的。
  5. 代码级缓存(细活): 库里直接调函数,粒度自己控、过期策略自己定, 代价是代码里全是缓存行——适合把单个高成本计算的结果存下来。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

  1. 定义策略时要存得可取(deny-all/allow-all 混合也行);
  2. 校验请求时按请求里的 key 一次调用取出相关策略;
  3. 策略变更时必须让缓存失效——否则按过期策略放行,等于门户大开。

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)→ 回源 → 重灌 → 后续命中。

  1. ACL 答 Who/Where/What;RBAC 再加 How/When,角色按职责定义、可继承。
  2. 动态权限 = 策略进可写存储; 管理员的战场在 UI,不在代码。
  3. Casbin 三词:Subject/Object/Action;四件接管、两件不管(认证、用户管理)。
  4. PERM 元模型四段:请求、策略、效果、匹配; RBAC = ACL + g = _, _ + 角色匹配器。
  5. 策略名字是裸字符串——用户角色与资源角色严禁同名。
  6. 缓存四策略按场景选; 五种形态对应架构里不同的层。
  7. 先无缓存跑通,确认瓶颈再上缓存——这是全书的纪律。
  8. guard 防并发重复计算昂贵的值; get_or_insert_with 简单但可能重复。
  9. 失效两档:remove 单个、clear 全体; 权限这类相互关联的数据,变更用 clear 最稳。
  10. 授权是缓存的头号候选;但「无缓存也必须正确」——缓存只买速度,不买正确性。

12. 原文地图

主题原书章原文位置
ACL 三问与 GBACAdministration, Dynamic ACLs, and Cachingtext/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

  1. 出处:「Access control lists」(text/81-fm-introduction.txt:74,搜「Who: The entity」;:84,搜「GBAC」)。 ↩

  2. 出处:「Role-based access control」(text/81-fm-introduction.txt:93,搜「hierarchy」)、:100(搜「How: The alteration」)、:103(搜「When: The access rights」)。 ↩

  3. 出处:「Role-based access control」(text/81-fm-introduction.txt:117,搜「considerable overlap」)。 ↩

  4. 出处:「User groups」(text/81-fm-introduction.txt:130,搜「Create ticket」)、:137(搜「Create project」)、:144(搜「Create user」)。 ↩

  5. 出处:「User groups」(text/81-fm-introduction.txt:152,搜「outsource this responsibility」)。 ↩

  6. 出处:「Dynamic role management」(text/81-fm-introduction.txt:158,搜「accessible medium」)、:168(搜「allow-all or deny-all」)。 ↩

  7. 出处:「Overview of Casbin」(text/81-fm-introduction.txt:192,搜「Subject」)、:204(搜「Policy enforcement」)、:230(搜「out of scope」)、:244(搜「second layer」)。 ↩

  8. 出处:「Getting started with Casbin」(text/81-fm-introduction.txt:284,搜「Enforcer::new」)、:308(搜「enforce」)。 ↩

  9. 出处:「Modeling with Casbin」(text/81-fm-introduction.txt:332,搜「PERM」)。 ↩

  10. 出处:「Modeling with Casbin」(text/81-fm-introduction.txt:354,搜「request_definition」)、:403(搜「alice, data1, read」)、:413(搜「No other options are allowed」)。 ↩

  11. 出处:「Modeling with Casbin」(text/81-fm-introduction.txt:422,搜「role_definition」)、:483(搜「g(r.sub」)、:1513(搜「transitivity」)。 ↩

  12. 出处:「Modeling with Casbin」(text/81-fm-introduction.txt:499,搜「interfere」)。 ↩

  13. 出处:「Platform administration」(text/81-fm-introduction.txt:522,搜「SeaORM」)、:551(搜「next authorization attempt」)。 ↩

  14. 出处:「Caching」(text/81-fm-introduction.txt:560,搜「Maurice Wilkes」)。 ↩ ↩2

  15. 出处:「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

  16. 出处:「Cache eviction strategies」(text/81-fm-introduction.txt:611,搜「best fits our purposes」)。 ↩

  17. 出处:「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」)。 ↩

  18. 出处:「Code level caches」(text/81-fm-introduction.txt:722,搜「Moka」)、:728(搜「Quick Cache」)、:1044(搜「Comparison of three」)。 ↩ ↩2

  19. 出处:「Code level caches」(text/81-fm-introduction.txt:1155,搜「without caching first」)。 ↩ ↩2

  20. 出处:「Retrieving data from the cache」(text/81-fm-introduction.txt:1199,搜「get_or_insert_with」)、:1220(搜「more than one」)。 ↩

  21. 出处:「Retrieving data from the cache」(text/81-fm-introduction.txt:1229,搜「get_value_or_guard_async」)、:1223(搜「produced only once」)。 ↩ ↩2

  22. 出处:「Invalidating the cache」(text/81-fm-introduction.txt:1254,搜「Remove a single entry」)、:1266(搜「flush all privileges」)。 ↩ ↩2

  23. 出处:「Caching and authorization」(text/81-fm-introduction.txt:1272,搜「most frequently accessed」)、:1285(搜「invalidate that cache」)。 ↩

  24. 出处:「Caching and authorization」(text/81-fm-introduction.txt:1289,搜「indifferent」)。 ↩

  25. 出处:「Conclusion」(text/81-fm-introduction.txt:1322,搜「loosely coupled」)。 ↩

  26. 出处:脚注 1(text/81-fm-introduction.txt:1489,搜「In practice」)。 ↩

On this page