KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

07 · 缓存与一致:权限改了多久生效 — keel 龙骨

权限判定几乎必然要缓存(每次请求查三张表是不可接受的),但权限是低频写、高频读的数据,而且它的变更后果不对称:生效慢了是体验问题,失效早了是安全事故。这一章讲缓存什么、key 怎么设计、怎么失效,以及权限系统自身需要哪些可观测性。

权限判定几乎必然要缓存(每次请求查三张表是不可接受的),但权限是低频写、高频读的数据,而且它的变更后果不对称:生效慢了是体验问题,失效早了是安全事故。这一章讲缓存什么、key 怎么设计、怎么失效,以及权限系统自身需要哪些可观测性。


一、现场:管理员刚给用户加了权限,用户说还是看不到

管理员在后台给 Bob 的角色加了 system:user:list,Bob 刷新页面:

另一个方向的事故:员工离职,管理员在后台禁用了他。5 分钟内他仍然能正常操作。

这两件事是同一个设计决策的两面:缓存时长 = 权限变更的最大生效延迟。

二、概念边界:缓存什么、不缓存什么

数据 能不能缓存 理由
权限点集合(member → codes) 能,且应该 低频变、高频读;重复查库代价高
角色定义(role 表) 能 几乎不变
成员身份(user → member) 要谨慎 变更低频但后果严重(离职、禁用、降租户权限)
ACL 条目 能,但粒度要小 条数多,要按资源 id 缓存
数据范围的可见组织列表 最需要谨慎 它直接决定能看到哪些行,失效早 = 数据泄露

分级原则:功能权限可以宽松缓存(最多延迟一次权限授予),
数据权限必须严格(权限收回应当立即或极快生效)。

一个常见的错误是"全部统一 TTL 5 分钟",它同时造成了上面两个现象。

三、一次完整运行:缓存 key 怎么设计

以权限集合为例,key 的设计决定了会不会串号,这是本章最需要小心的地方:

# ✅ 正确:key 里必须包含身份与租户
PERM_KEY = f"perm:v1:{tenant_id}:{member_id}"
# 权限授予/角色变更时删除:DEL perm:v1:{tenant}:{member}:*

# ❌ 错误:只用 user_id
PERM_KEY = f"perm:{user_id}"      # 同一个人在两个租户会互相覆盖

# ❌ 更危险:只按角色缓存后拼接
# 因为角色是租户内数据,跨租户拼角色码会拿到别的租户的权限

第二条为什么危险,看配套项目的演示数据就明白了:

  u_alice @ t_acme     -> member=m_alice_acme    roles=('tenant_admin',)
  u_alice @ t_globex   -> member=m_alice_globex  roles=('viewer',)

如果缓存 key 用 user_id,Alice 在两个租户的权限会互相覆盖——
于是可能出现"在 A 租户被判成访客、在 B 租户被判成管理员"这类极难解释的现象。

凡是多租户系统的缓存,key 里必须有租户维度。这不是建议,是这类 bug 的唯一解法。

版本号方案:比主动失效更稳的一种做法

主动失效(改权限时 DEL key)有个现实问题:可能漏。漏在哪?

版本号方案把这件事变成"必然正确":

# 身份版本号:角色变更时 +1
version = get_member_version(tenant_id, member_id)      # 通常来自 Redis 或 member 表的一列
perm_key = f"perm:v{version}:{tenant_id}:{member_id}"

# 失效 = 让版本号变大,而不是去删某个 key
def bump_version(tenant_id, member_id):
    incr(f"memberver:{tenant_id}:{member_id}")

好处是不依赖"记得删":老 key 自然过期,新请求自动用新版本号。
代价是要多一次 Redis/DB 读(查版本号),以及需要一张表或一个字段承载版本。

四、失败注入:三个能真实复现的一致性问题

问题 1:缓存 key 漏了租户维度

Alice 登录 acme  → 缓存 perm:u_alice = {system:user:delete, ...}(管理员权限)
Alice 登录 globex → 命中同一个 key → 拿到管理员权限

配套项目里能直接复现:把 PERM_KEY 里的 member_id 换成 user_id,
然后用同一个 u_alice 分别请求两个租户,观察 api_probe.py 的"同一账号换租户"那行
从 roles: ['viewer'] 变成管理员。

问题 2:只失效了部分 key

用户在两个终端登录(不同城市/不同设备 → 可能有不同的实例或不同的 key 命名空间)。
管理员改了权限,失效只清了其中一个实例的 key。表现是"换个设备就好了"。

问题 3:数据范围缓存失效延迟

这是最严重的一类:把 visible_org_ids 的结果缓存起来,离职员工的可见组织列表
在 TTL 内仍然有效 → 已经离职的人仍能看到部门数据。

配套项目的数据范围是实时查的(rbac.visible_org_ids 每次查库),
所以它"慢"但不"漏"。这是一个有意的取舍,02 章讲过数据权限是过滤器,
过滤器的失效等于数据泄露。

五、可观测性:权限系统需要哪些指标

权限出问题时,"查日志"往往查不到——因为没人知道用户的权限是什么时候变的。最小可用的一组:

指标 为什么需要
拒绝事件计数(按权限点、按原因) 突增通常意味着权限配置变更或攻击试探
TenantMismatch 计数 突增 = 有人在猜租户 id,或者前端租户切换坏了
ResourceNotFound 计数 突增 = 有人在扫 id(05 章的攻击形态)
权限缓存命中率 命中率骤降 = 权限查询变慢或缓存故障
权限变更审计(谁、何时、给谁、加了什么) 事故复盘的唯一依据
生效延迟(变更时间 → 目标用户首次生效时间) 直接量化"权限改了多久生效"

一条容易被忽略的指标:权限变更的生效延迟。它不是性能指标而是产品指标——
把权限系统接进公司流程之后,"改完多久生效"会变成经常被问的问题,能直接回答它的监控很省事。

六、误判澄清

误解 核对 结论
"权限变更要重新登录才生效" 取决于缓存失效策略 主动失效 / 版本号方案都能做到秒级,不必强制重登
"统一 TTL 5 分钟最省事" 授予延迟与收回延迟都变成 5 分钟 权限的收回比授予更敏感,需要分级
"删缓存就行" 直接改库、其他实例的变更不会触发 版本号方案不依赖"记得删"
"缓存 key 用 user_id 够用" Alice 在两个租户角色不同 多租户系统的缓存 key 必须含租户维度
"权限判定不该缓存" 每次三张表 缓存是必要的,问题在缓存什么、以及失效策略

七、生产环境怎样替换

教学替身 生产替换 要点
每次实时查库 权限集合缓存 + 版本号 配套项目保持实时,是为了让读者看清判定链路
无失效 主动失效 + 版本号双保险 版本号兜住"漏删"的情况
无审计 权限变更审计表 + 日志 事故复盘只能靠它
无指标 第 05 节的最小指标集 拒绝事件计数是发现越权试探的唯一低成本手段

八、收口:全课的一句话总结

把七章压成一条链路,每一层对应一种机制,缺一层就有一类漏洞:

认证        →  你是谁                凭证不可信客户端自称(00、04 章)
租户上下文   →  你在哪个空间          服务端从身份推导,不采信声明(04、05 章)
功能权限     →  你能做什么            声明式鉴权,声明与路由同处一行(01、03 章)
数据范围     →  你能看到哪些行        机制化注入,永不跨租户(02 章)
资源级别     →  这一条能到什么程度    带权重的 ACL,多来源取最高(06 章)
缓存与审计   →  变了多久生效         租户维度的 key + 版本号(07 章)

九、练习与验收

练习 1:审查你系统的权限相关缓存 key,逐个检查是否包含租户维度;找出"改库后不失效"的路径。

练习 2:给配套项目加一个 memberver 版本号方案,让"角色变更 → 权限生效"不依赖主动删 key。

练习 3:设计权限系统的最小指标集与告警阈值,特别是"权限变更生效延迟"这个产品指标的采集方式。

验收点:不看资料,说出哪些数据可以宽松缓存、哪些必须严格(数据权限),为什么;解释多租户缓存 key 必须含租户维度;说出主动失效与版本号方案的取舍;描述权限系统需要观测的指标。


现在能解释什么:权限缓存的分级原则与失效策略(主动失效 vs 版本号);多租户缓存 key 的租户维度要求;三个能真实复现的一致性问题;权限系统的最小可观测性集。

课程收尾:现在你应该能独立回答一个问题——"我们的权限系统安全吗"。方法不是"检查有没有鉴权代码",而是:拿五类越权尝试逐个试一遍(第 05 章),看每一类被挡在哪一层,并确认那一层是机制保证而不是某个人的记忆。

进入 keel 阅读