资讯详情

权限改了不生效?Gauzy 权限缓存与多租户的暗坑排查

📅 2026/10/9 20:56:17 | 华诺云谱 👁 阅读
权限改了不生效?Gauzy 权限缓存与多租户的暗坑排查
权限改了不生效Gauzy 权限缓存与多租户的暗坑排查【免费下载链接】ever-gauzyEver® Gauzy™ - Open Business Management Platform (ERP/CRM/HRM/ATS/PM) - https://gauzy.co项目地址: https://gitcode.com/GitHub_Trending/ev/ever-gauzy在 Ever Gauzy 这类多租户开源 ERP/HRM 平台NestJS TypeORM/MikroORM cache-manager上做权限二次开发或生产排障时我在后台给某个角色勾了权限用户却说还是进不去 / 还是能进去几乎是最常见、也最容易让人怀疑是自己改错的问题。这篇排障笔记直接钻进packages/core的守卫与角色权限服务源码把权限不生效拆成三种典型表现逐一还原背后的缓存机制、多租户隔离规则与 RBAC 模型的交叉点并给出一条可执行的排查路径。权限不生效的三种典型表现先对齐现象再看代码。把社区里反复出现的权限改了没用案例归类不外乎下面三种改完立刻生效但 5 分钟内延迟生效。管理员给某个角色勾选/取消一项权限后对应用户的后续请求仍然按旧权限判定最长要等约 5 分钟才突然正常。这不是前端问题而是后端权限缓存的 TTL 特性见下节。后端已放行前端菜单/页面却不变。登录后前端把权限列表快照保存在本地store / localStorage路由与页面 tab 的可见性由这份快照决定权限变更后只要不重新登录刷新快照界面就一直停留在旧状态。比如 dashboard.component.ts 中会计、HR、项目看板等 tab 是否注册依赖PermissionsEnum.ADMIN_DASHBOARD_VIEW、ACCOUNTING_DASHBOARD等权限而这份权限来自登录时拉的列表。同一账号换了租户/组织后权限错乱。权限记录、角色、组织策略都以tenantId为隔离维度请求上下文里的tenantId与数据实际归属一旦错位就会出现这个租户能做的操作换到另一个租户就莫名被拒/放行。三种表现的根因并不相同第一种在 permission.guard.ts 的缓存逻辑里第二种在前端权限快照第三种则要拆开看 tenant-base.guard.ts 的租户校验和 organization-permission.guard.ts 的组织级策略。缓存机制、多租户隔离与权限模型的交叉坑1. 三层守卫先租户、后权限以角色权限管理接口为例role-permission.controller.ts 的声明是UseGuards(TenantPermissionGuard, PermissionGuard) Permissions(PermissionsEnum.CHANGE_ROLES_PERMISSIONS) Controller(/role-permissions)一个请求进来实际要过四道闸AuthGuardJWT 认证→TenantPermissionGuard租户校验 权限校验→PermissionGuard权限校验→RoleGuard角色校验。后三层各自读取Permissions()/Roles()元数据permissions.decorator.ts 用SetMetadata写入PERMISSIONS_METADATA任何一个不通过都会拒绝。权限不生效先要定位是被哪一道闸拦下的这是排障的第一原则。2. 5 分钟权限缓存为什么改了不生效这是改了不生效最核心的机制。permission.guard.ts 的判定逻辑如下const tenantId RequestContext.currentTenantId(); const roleId RequestContext.currentRoleId(); const cacheKey userPermissions_${tenantId}_${roleId}_${permissions.join(_)}; let isAuthorized false; const fromCache await this._cacheManager.getboolean | null(cacheKey); if (fromCache null) { isAuthorized await this._rolePermissionService.checkRolePermission(tenantId, roleId, permissions, true); await this._cacheManager.set(cacheKey, isAuthorized, 5 * 60 * 1000); // 5 分钟 TTL } else { isAuthorized fromCache; }TenantPermissionGuard的逻辑几乎相同tenant-permission.guard.ts只是缓存键前缀换成tenantPermissions_OrganizationPermissionGuard则是orgPermissions_${tenantId}_${organizationIds.join(_)}_...同样是5 分钟 TTLorganization-permission.guard.ts。三个关键结论缓存键 租户 角色 被请求的权限集合。只要tenantId与roleId正确取自请求上下文RequestContext.currentTenantId()/currentRoleId()见 request-context.ts不同租户、不同角色之间理论上不会串缓存。写权限接口并没有显式清理这几类缓存键。role-permission.service.ts 的createPermission/updatePermission/deletePermission只做数据库写入与角色守卫校验role-permission.controller.ts 的增删改路由也没有cacheManager.del。因此改完不生效是 TTL 到期前的预期行为而非数据没写进去。数据是否写入成功要看role_permission表。写入时还要过updatePermission里那套SUPER_ADMIN 不能改自己、ADMIN 不能改 SUPER_ADMIN/ADMIN的规则role-permission.service.ts不满足会被NotAcceptableException直接拒绝——这本身也可能被误报成权限不生效。3. 角色状态 60 秒缓存与 JWT claim 冻结缓存不止守卫那一层。jwt.strategy.ts 在每次请求的认证阶段会调用RoleAuthorizationService.attachAuthorizationState(user)把此刻数据库里的角色名与启用权限挂到request.user上role-authorization.service.ts 对这个查询做了缓存private static readonly CACHE_KEY_PREFIX authz_role_state_; private static readonly CACHE_TTL_MS 60 * 1000; // 60 秒这里需要理解两层时间尺度JWT 里的role/permissionsclaim 是签发时冻结的最长可存活一个 token 生命周期默认 24 小时。代码注释明确记载曾经因为直接读取 token claim 做鉴权被降权的用户在其 token 过期前仍保有原权限GHSA-m8xc-8pwr-89fj 类问题。现在改成每次请求从数据库重载角色状态缓存只有 60 秒比守卫的 5 分钟更短。所以权限修改后的生效时间实际是两层缓存叠加角色状态 60 秒 守卫判定 5 分钟。对用户而言最坏情况约 5 分钟后所有请求都按新权限判定。4. 多租户隔离的隐性规则不带 tenantId 的请求可能被直接拒绝权限数据本身按租户隔离——RolePermission实体在(tenantId, roleId, permission)上建有唯一索引role-permission.entity.tscheckRolePermission的 SQL 也以rp.tenantId :tenantId为第一条件。但真正的坑在 tenant-base.guard.ts 的上下文校验const httpMethods [RequestMethodEnum.GET, RequestMethodEnum.DELETE]; if (httpMethods.includes(method)) { if (tenantId in query) { isAuthorized currentTenantId query[tenantId]; } else if (query.hasOwnProperty(data)) { // 解析 JSON 中的 findInput.tenantId } else { isAuthorized false; // 既无 tenantId 也无 data直接拒绝 } } const payloadMethods [RequestMethodEnum.POST, RequestMethodEnum.PUT, RequestMethodEnum.PATCH]; if (payloadMethods.includes(method)) { const body: any request.body; let bodyTenantId: string; if (tenantId in body) { bodyTenantId body[tenantId]; } else if (tenant in body) { bodyTenantId body[tenant][id]; } isAuthorized currentTenantId bodyTenantId; }也就是说GET/DELETE 请求必须显式携带与当前用户一致的tenantIdquery 或 data 参数或tenant-id请求头POST/PUT/PATCH 必须在 body 里带上一致的tenantId否则即使角色权限完全正确也会被拒绝。这条规则是权限不生效里最隐蔽的一类权限数据没问题但请求没有把租户上下文申报清楚。常见触发场景包括二次开发时新增的查询接口忘了带 tenantId 过滤、前端切换租户后没同步更新请求体、以及某些批量接口把 tenantId 放在 body 而守卫读的是 query。5. 组织级策略第三个独立的权限维度Gauzy 还有一类与角色权限正交的开关组织的时间跟踪策略allowManualTime / allowModifyTime / allowDeleteTime。organization-permission.guard.ts 用ORGANIZATION_POLICY_COLUMNS白名单把这三个权限映射为organization表字段再对请求涉及的每一个组织做行数比对count organizationIds.length任何一个组织关闭该开关都会被拒。所以用户明明有ALLOW_MANUAL_TIME权限却无法手动补录工时时请先检查目标组织行的allowManualTime字段——这是角色权限之外的组织维度在权限模型里同样算作权限不生效。从现象到根因的排查路径综合上面的机制给出按成本从低到高的排查顺序。每步都能在代码里找到对应的判定点排查时打开应用日志守卫本身会打印关键信息Checking User Permissions from Cache with key: ...、Tenant Permissions NOT loaded from Cache ...、Guard TenantBase: Unauthorized access blocked. TenantId: ...permission.guard.ts、tenant-permission.guard.ts、tenant-base.guard.ts。第 0 步确认数据真的写进去了。查role_permission表SELECT roleId, permission, enabled, isActive, isArchived, tenantId FROM role_permission WHERE tenantId 当前租户 AND roleId 目标角色 AND permission IN (...);判定以enabled true AND isActive true AND isArchived false为准与 role-permission.service.ts 的checkRolePermissionSQL 条件一致。若记录缺失或enabled false问题在写入层而非缓存层若写入时收到NotAcceptableException是被 role-permission.service.ts 的角色层级保护拦下ADMIN 无权给 ADMIN/SUPER_ADMIN 改权限。第 1 步排除缓存滞后。权限已正确写入但用户侧仍不生效先看日志中的缓存键与from Cache分支。等待 5 分钟或重启缓存服务/清空 cache-manager后复测若立刻生效就是两层缓存守卫 5 分钟 角色状态 60 秒的 TTL 行为属预期设计。生产环境若要求秒级生效方向是给写权限接口补上对应缓存键的主动失效或在守卫层把 TTL 缩短。第 2 步核对租户上下文。检查请求是否满足 tenant-base.guard.ts 的申报规则GET/DELETE 是否带tenantIdquery/data/tenant-id头POST/PUT/PATCH 是否在 body 带一致的tenantId。再核对 JWT 中签发的tenantId与当前选择的工作区租户是否一致——租户上下文错位时日志会打印Guard TenantBase: Unauthorized access blocked。第 3 步检查组织维度。若涉及时间跟踪类操作确认目标组织行的allowManualTime / allowModifyTime / allowDeleteTime字段值以及请求涉及的每个组织body、query、params、token 中lastOrganizationId是否全部放行——organization-permission.guard.ts 要求所有组织都允许才通过。第 4 步刷新前端权限快照。后端判定已通过但前端界面不变时让用户重新登录以重新拉取权限列表dashboard tab、菜单、路由守卫都依赖这份快照见 dashboard.component.ts。这一步与后端缓存无关是常见误判来源。第 5 步确认 token 身份与当前角色一致。jwt.strategy.ts 每次请求都会用数据库中的roleId重新解析角色与权限并挂到request.userrequest-context.ts 的hasPermissions / hasRoles一律读取这份DB 实时状态而非 token claim。因此角色被删除、roleId缺失时会 fail-closed拒绝这也是权限莫名消失的一个排查点。小结Ever Gauzy 的权限不生效本质是三层独立机制叠加的结果守卫层的 5 分钟权限缓存permission.guard.ts / tenant-permission.guard.ts、认证层的 60 秒角色状态缓存role-authorization.service.ts以及多租户/多组织的上下文校验tenant-base.guard.ts / organization-permission.guard.ts。排查时先看数据、再看缓存日志、三看请求上下文申报、四看组织开关通常十分钟内就能把玄学收敛成某一个确定层的缺陷——要么补缓存失效要么补租户参数要么重登刷新快照而不是盲目改库或重启服务。【免费下载链接】ever-gauzyEver® Gauzy™ - Open Business Management Platform (ERP/CRM/HRM/ATS/PM) - https://gauzy.co项目地址: https://gitcode.com/GitHub_Trending/ev/ever-gauzy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑