KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
07 · 缓存与一致:权限改了多久生效 — keel 龙骨
权限判定几乎必然要缓存(每次请求查三张表是不可接受的),但权限是低频写、高频读的数据,而且它的变更后果不对称:生效慢了是体验问题,失效早了是安全事故。这一章讲缓存什么、key 怎么设计、怎么失效,以及权限系统自身需要哪些可观测性。
权限判定几乎必然要缓存(每次请求查三张表是不可接受的),但权限是低频写、高频读的数据,而且它的变更后果不对称:生效慢了是体验问题,失效早了是安全事故。这一章讲缓存什么、key 怎么设计、怎么失效,以及权限系统自身需要哪些可观测性。
一、现场:管理员刚给用户加了权限,用户说还是看不到
管理员在后台给 Bob 的角色加了 system:user:list,Bob 刷新页面:
- 菜单出来了(前端重新拉了一次菜单);
- 但接口仍然 403(后端缓存里还没有这个权限点);
- 5 分钟后自己好了(TTL 到期)。
另一个方向的事故:员工离职,管理员在后台禁用了他。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)有个现实问题:可能漏。漏在哪?
- 直接改库(运维执行 SQL、数据修复脚本)不会触发失效;
- 另一个服务/另一个实例改了角色,本实例不知道;
- 批量授权走了不同代码路径。
版本号方案把这件事变成"必然正确":
# 身份版本号:角色变更时 +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 章),看每一类被挡在哪一层,并确认那一层是机制保证而不是某个人的记忆。