权限体系总览
适用端:后端 + Web | 类型:机制
定制功能
sys_ 前缀的 7 张表、决策引擎、行级规则、字段模式、记录共享、权限诊断——整套都是本项目自研。上游 yudao 只有 system_role_menu(角色↔菜单的多对多)加一个 @DataScope 注解。本页讲的是这套自研体系;上游那套(system_menu / system_role_menu / @DataScope)见路由与权限机制,逐域的约定与扩展点留给 04 上游域开发章(待整理)。
这一页是 02 域的地图。权限体系有三层(功能 / 行 / 列字段),三层各有独立的存储、判定入口和失效模式,而且新旧两套并存(开关切换)。搞不清自己在改哪一层,是这套体系里最常见的错误来源。
前置阅读:路由与权限机制(前端那层:路由守卫、菜单渲染、v-hasPermi)、数据库约定与表总览。
它解决什么问题
三层各管一段,任何一层放行都不代表整体放行
- 功能权限判「能不能调这个接口」,返回 boolean。
- 行级判「这个接口能查出哪些行」,产物是一段 SQL 条件。
- 列/字段级判「查出来的行里哪些列能看/能改」,产物是
EDITABLE/READONLY/HIDDEN三态。
三层不共享判定结果。给一个用户配了 query 的 ALLOW,不会自动让他看到任何一行数据;行过滤放开了,也不会自动让他看到敏感列。
新旧两套并存
两个开关都在 enlev.permission 下,PermissionProperties 的默认值与 application.yaml 的实际值不一致,以 yaml 为准:
| 配置项 | PermissionProperties 默认 | application.yaml 实际 | 含义 |
|---|---|---|---|
grant-enabled | false(:27) | true(:351) | 走新引擎(sys_resource + sys_grant)还是旧路径(system_role_menu) |
collapse-enabled | false(:47) | true(:378) | 走「资源身份 × 动作」塌缩后的读路径 |
grant-migrate-on-startup | false(:60) | 未配置 | 启动期是否跑 system_role_menu → sys_grant 迁移 |
derived-key-warn-threshold | 5000(:78) | 5000(:381) | 派生可见性的业务主键数告警阈值 |
subject.post-enabled | true(:88) | 未配置 | 岗位是否作为授权主体 |
subject.dept-enabled | false(:100) | true(:396) | 部门是否作为授权主体 |
subject.dept-ancestor-enabled | true(:109) | 未配置 | 部门主体是否含祖先部门 |
yaml 对 grant-enabled 的注释记着切换前的对账(S5c):旧路径放行 1549 对(角色,权限),新引擎 1550 对,旧有新无 = 0,即没有任何权限点会因切换而被误杀。
PermissionServiceImpl:112 的注释已过期
那段注释写着「grant-enabled = false(默认)→ 走旧路径,保证现有环境不受影响……迁移脚本跑完、验证权限点都正常后,改为 true 才真正走新路径」。而 yaml 里它早就是 true 了。读代码时别被这段注释带走——它描述的是一个已经完成的迁移过程。
塌缩开关是本项目已知的最严重缺口
application.yaml:371-377 的注释逐字承认:FormDesignRuntimeController#checkPermission 比的是带动词的 infra:form-design-run:{code}:{action},塌缩后它被翻译成同一个身份行去查;身份行 default_effect=ALLOW 且链上零授权时,判定回落成放行。实测已打穿 3 张已发布表单(非超管、零授权,/config 与 /page 均返回 code=0 与真实数据行)。权威副本在 GrantDecisionService#lookupOf 的注释里。完整口径、处置与三条被否掉的根治路线见授权与决策引擎。
代码在哪
后端路径前缀 enlev-module-system/src/main/java/cn/enlev/ainb/module/system/:
判定链
| 职责 | 文件 | 关键方法 |
|---|---|---|
@ss 的实现(@PreAuthorize 打到这里) | framework/security/core/service/SecurityFrameworkServiceImpl.java | hasPermission / hasAnyPermissions |
| 超管短路 + 新旧开关 | service/permission/PermissionServiceImpl.java | hasAnyPermissions:96、syncRoleMenuGrants:233、filterMenuDerived:468 |
| 决策引擎(纯函数) | framework/permission/core/decision/GrantDecisionEngine.java | explain:132、isAllowed:114、Decision:54、DecisionReason:77 |
| 决策服务(装配 + 缓存 + 塌缩翻译) | service/permission/GrantDecisionService.java | hasPermission:416/446、lookupOf:172、chainsOf:294、listAllowedPermissions:936、listAllowedMenuIds:783、listEffectiveTree:1145、resolveFieldModes:589 |
| 主体闭包 | service/permission/SubjectClosureServiceImpl.java | — |
| 主体寻址 | framework/permission/core/subject/{SubjectKey, SubjectTypeEnum}.java | isRefBased:105、parse:116 |
| 配置 | framework/permission/config/PermissionProperties.java | 前缀 enlev.permission |
行级与列级
| 职责 | 文件 | 关键方法 |
|---|---|---|
| 行过滤求值 | framework/permission/core/rowfilter/DataRowFilterResolver.java | resolve:164、isSuperAdmin:341、combine:363、expandBuiltInTemplate:674 |
| MyBatis 拦截器接入 | framework/permission/core/rule/RuleDataPermissionRule.java | getTableNames()、registerTable() |
| 条件树求值 | framework/permission/core/rule/ConditionTreeEvaluator.java | resolveColumn:279、resolveQualifier:303 |
| 字段裁剪(原生模块) | enlev-framework/enlev-spring-boot-starter-biz-data-permission 下 PermissionFieldHelper / PermissionFieldCropAspect | fields(Class) |
| 字段裁剪(在线表单) | infra service/formdesign/FormFieldCropper.java | resolve:79 |
| 行过滤(在线表单) | infra framework/formdesign/core/FormRowFilter.java | resolve:91、executeIgnore:123 |
资源、规则、共享、诊断
| 职责 | 文件 | 关键方法 |
|---|---|---|
| 资源 CRUD + 菜单同步 | service/permission/ResourceServiceImpl.java | syncFromMenu:278、syncCarrierFromMenu:387、carrierDisplayName:440、repointCarrierToSuccessor:463、buildFromMenu:601 |
| 命名约定 | framework/permission/core/dataset/DatasetResourceNaming.java | dataset()、child()、RUN_PERMISSION_PREFIX |
| 权限串解析(塌缩翻译) | framework/permission/core/collapse/PermissionStringResolver.java | Tier(6 级) |
| 树推导 | framework/permission/core/PermissionTreeDeriver.java | — |
| 塌缩计划器 | framework/permission/core/collapse/ResourceCollapsePlanner.java | applyStrictestDefaultEffect:584、blockedIdentities:290、chooseMenuId:653 |
| 塌缩定位器 | framework/permission/core/collapse/CollapsedResourceLocator.java | identityOfAll:99 |
| 数据集登记(表单/原生) | service/permission/DatasetResourceServiceImpl.java | register:88、applyDefaultEffectOnTypeUpgrade:293 |
| 记录共享 | service/permission/RecordShareService.java、controller/admin/permission/RecordShareController.java | checkShareable:73-104、share:120 |
| 诊断 | controller/admin/permission/PermissionDiagController.java | 1 个端点 |
| 启动 Runner ×6 | framework/permission/runner/ | @Order(1)→@Order(6),见低代码总览 |
枚举(全部逐字核对)
| 枚举 | 取值 | 落库位置 |
|---|---|---|
ResourceTypeEnum | MENU / ACTION / PROCESS / CATEGORY / DATASET / COLUMN / FIELD(7 个) | sys_resource.type |
GrantActionEnum | VIEW / CREATE / UPDATE / DELETE / EXPORT / IMPORT / PRINT / START / DESIGN / MANAGE / VIEW_ALL / REFERENCE / WILDCARD("*")(13 个) | sys_grant.action |
GrantEffectEnum | ALLOW / DENY | sys_grant.effect |
SubjectTypeEnum | USER / ROLE / DEPT / POST / GROUP / SYSTEM / ALL(7 个) | sys_grant.subject_type |
FieldModeEnum | EDITABLE(2) / READONLY(1) / HIDDEN(0) | sys_grant.field_mode |
PermissionSlotEnum | owner / dept / status / bizKey | sys_slot_mapping.slot |
RuleTemplateEnum | 10 个内置模板(见行过滤规则) | sys_rule.template |
ConditionOperatorEnum | EQ / NE / GT / GTE / LT / LTE / IN / NOT_IN / IS_NULL / IS_NOT_NULL / LIKE / NOT_LIKE(12 个) | 条件树 JSON |
RecordSharePolicyEnum | OWNER_ONLY(默认)/ ANY_VIEWER / DISABLED | sys_resource.share_policy |
RoleCodeEnum | super_admin / tenant_admin / crm_admin | system_role.code |
GrantDecisionEngine.DecisionReason | NO_SUBJECT / EMPTY_CHAIN / DEFAULT_EFFECT / GLOBAL_DENY / CONTEXT_DENY / CONTEXT_ALLOW / GLOBAL_ALLOW / NO_MATCHING_EFFECT(8 个,顺序即优先级) | 不落库 |
⚠️ 枚举名直接落库(varchar),不要改名。SubjectTypeEnum:14-15 说明了用 varchar 而不是 tinyint 的理由:主体表与成员表是低频配置数据,量级在千行以内,省那几个字节没有意义,而排查权限问题时能直接 select 出 ROLE/DEPT 而不是 2/3,价值很大。
核心机制
权限串命名约定
在线表单派生的权限串有严格形态,DatasetResourceNaming 是唯一出处:
| 形态 | 例子 | 资源类型 |
|---|---|---|
infra:form-design-run:{formCode} | infra:form-design-run:doc_device_ledger | DATASET(身份行) |
infra:form-design-run:{formCode}:{action} | ...:doc_device_ledger:query | ACTION(塌缩后退役) |
infra:form-design-run:{formCode}:col:{key} | ...:doc_device_ledger:col:phone | COLUMN |
infra:form-design-run:{formCode}:fld:{key} | ...:doc_device_ledger:fld:phone | FIELD |
原生模块走 @PermissionDataset(value = "system:user"),串就是那个 value,子级同 :col: / :fld: 规则。
用错重载会静默产出一个「不成树」的权限串
DatasetResourceNaming.child() 有多个重载。传错参数不会抛异常,只会拼出一个形态合法但在树里找不到父节点的串——表现是「资源建出来了、授权也配了、判定永远算不到它」。改这块前先读方法签名,别凭参数名猜。
主体闭包:谁在替我说话
一个用户不是单一主体。SubjectClosureServiceImpl 把他展开成一个 Set<SubjectKey>:
ALL 主体永远在每个登录用户的闭包里(SubjectTypeEnum:80)。所以给 ALL 配 DENY 等于对所有人关闭——这是刻意的,「先全关再逐个开」是常见的收紧手法。
它存在的理由(:69-78)值得记住:「不限制」在授权模型里必须是一条显式的授权,不能是「没有授权」。流程资源的 default_effect 是 DENY,所以「这个流程谁都能发起」原本表达不出来,只能靠 startUserIds 两个范围都为空时的回落分支兜着;回落一摘,所有没配 START 授权的流程立刻变成谁都发不了。有了 ALL,「不限制」就成了一条看得见、可审计、可在界面上撤销的授权行。
sys_subject_member 的方向反了就是静默提权
成员表的语义是「subject_id 这个组里有 member_type:member_ref_id 这个成员」。写反方向不会报错——闭包照样算得出来,只是算出来的是「谁属于我」而不是「我属于谁」,结果是静默提权。这张表没有任何数据库层约束能挡住方向写反。
决策:八步
GrantDecisionService.hasPermission → GrantDecisionEngine.explain 的完整链路:
- 超管短路。
PermissionServiceImpl.hasAnyPermissions:96先查角色,hasAnySuperAdmin为真直接return true,根本不进引擎。 - 开关分流。
grant-enabled=true走新引擎,否则走旧路径hasAnyPermissionLegacy。 - 塌缩翻译。
collapse-enabled=true时,带动词的串被PermissionStringResolver翻译成「身份行 + 动作」去查(lookupOf:172)。 - 算主体闭包。
- 算资源祖先链。物化路径
path,WHERE :myPath LIKE CONCAT(path, '%'),不递归。 - 引擎判定。
explain:167-173用五个 filter 选出命中行:isActive()(有效期)、chainIds.contains(resourceId)、subjectMatches、actionMatches(action) || actionMatches("*")、isGlobal() || contextId 匹配。 - 优先级。全局 DENY > 上下文 DENY > 上下文 ALLOW > 全局 ALLOW。
- 回落。命中行为空时,读链上最具体那个资源的
default_effect列(不是枚举方法)。
isAllowed 是 explain 的薄封装,这是构造上的保证
isAllowed:114-120 全文就一句 return explain(...).allowed()。Decision 的类注释(:36-44)解释了为什么必须这样:权限地图(listEffectiveTree)要展示「这项权限从哪来」,而鉴权只要一个 boolean;如果两边各算各的,就又多了一份会漂移的口径——这套体系已经漂过两次了(buildResourceChain 沿 path 上溯不过滤 status vs buildChainFromMap 沿 parentId 上溯只含启用资源,在「停用祖先」场景分叉;GrantAdminController#listByResource 与 toRespVOs 两处 expired 口径也不一致)。焊死在同一段代码里,两者在构造上不可能不一致。
四处优先级判定一律用 findFirst 而不是 anyMatch(:189-190):判定结果完全一样,但顺手拿到了「是谁投的这一票」,权限地图靠它区分「本资源配置」与「继承自上级」。
default_effect:一个刻意的分叉
这是整套体系里最容易算错的地方,因为有两个 default_effect,且它们不相等:
ResourceTypeEnum.defaultEffect() | sys_resource.default_effect 列 | |
|---|---|---|
| 是什么 | 枚举方法,按类型返回出厂值 | 数据库列,每行各自的值 |
| 谁读它 | 只有 ResourceAdminController#createResource 填默认值时 | 判定时读的是它 |
| DATASET | ALLOW | 可能被塌缩计划器或管理员改成 DENY |
ResourceTypeEnum.defaultEffect():73-75:(DATASET || COLUMN || FIELD) ? "ALLOW" : "DENY"。
DATASET 出厂 ALLOW 是刻意设计——:65-71 的 javadoc 写着它「不是访问控制点……若把 DATASET 设成 DENY,零配置环境下所有在线表单会被全面拒访」。真正的控制点是它下面那 6 个 default_effect=DENY 的 ACTION 行。
一句话解释塌缩缺口
DATASET=ALLOW 是刻意设计(它不是访问控制点,控制点是它下面 6 个 DENY 的 ACTION 行);塌缩把那 6 个 ACTION 成员行退役后,判定改查 DATASET 身份行,于是「刻意设计的非控制点」被当成了控制点。
引擎读列而不读枚举,:178-181 给了实测反例:库里 PROCESS 类型有 18 行 default_effect=ALLOW、9 行 DENY,而枚举说 PROCESS 该是 DENY——按 type 推就会算错那 18 个节点。
⚠️ ResourceDO.java:123-127 的 javadoc 与 DDL 的列注释都还写着 DATASET 是 DENY,均已过期。
两个早退不回落
explain:137-143 的两个早退都不回落 default_effect:主体集合为空 → NO_SUBJECT,资源链为空 → EMPTY_CHAIN,直接拒绝。没有身份 / 没有资源,就没有权限可言。
塌缩之后「资源链为空」不再等于拒绝
塌缩前 chains.isEmpty() 直接 return false。塌缩后动词串被翻译成身份行 + 动作,于是删掉一个按钮资源不再表示拒绝:身份行 default_effect=ALLOW、链上零授权 → 回落放行。GrantDecisionService:200-285 把这个缺口连同实测数据、严重度纪律和三条被否掉的根治路线写全了。
三层怎么在一次请求里叠加
以在线表单 POST /infra/form-design-run/{code}/page 为例:
| 层 | 在哪介入 | 产物 | 失败模式 |
|---|---|---|---|
| 功能权限 | FormDesignRuntimeController.checkPermission:363-368,编程式(串里含 code,@PreAuthorize 静态字符串做不到) | boolean,拒绝则抛业务异常 | 塌缩回落放行 |
| 行级 | FormRowFilter → FormDesignSqlBuilder 6 个注入点(:281,404,420,446,491,659),JdbcTemplate 拼原生 SQL,不经过 MyBatis 执行器 | 一段 WHERE | 两个降级方向相反:PermissionCommonApi 不可用 → pass() 不过滤;求值抛异常 → denyAll() |
| 列/字段级 | FormFieldCropper 在 Service 层,三个出口 config(:292,295)/get(:1236)/page 必须一起裁 | 反射把值置 null(不是不查那一列) | 只裁 config 的话抓包就能看到明文 |
原生模块(system_users 这些)走的是另一套:行级在 RuleDataPermissionRule(MyBatis 拦截器),列级在 PermissionFieldCropAspect(AOP)。详见行过滤规则。
数据模型
7 张表,全部 sys_ 前缀(上游用 system_),完整列清单见 docs/.generated/tables-permission.md(由 tools/gen-tables.mjs 从 information_schema 生成,不是站点页面)。
| 表 | 一句话 | 租户维度 |
|---|---|---|
sys_resource | 可授权对象的树(7 种类型) | 🔴 @TenantIgnore 全局表 |
sys_grant | 授权行:谁 × 哪个资源 × 什么动作 × ALLOW/DENY | TenantBaseDO 分租户 |
sys_rule | 行级规则(模板 + 条件树 + 参数) | 🔴 @TenantIgnore 全局表 |
sys_slot_mapping | 语义槽位 → 物理列名 | 🔴 @TenantIgnore 全局表 |
sys_subject | 用户组(GROUP 主体自己的实体) | TenantBaseDO 分租户 |
sys_subject_member | 组成员关系 | TenantBaseDO 分租户 |
sys_record_share | 单条记录分享给某主体 | TenantBaseDO 分租户 |
7 张表都有 tenant_id 列,但只有 4 张真的按它过滤
ResourceDO / RuleDO / SlotMappingDO 打了 @TenantIgnore 且继承 BaseDO——列保留但不参与读写。ResourceDO:42-45 记着踩过的坑:
本类最初继承
TenantBaseDO,而资源行是启动期迁移进来的(tenant_id = 0)。于是租户 1 的用户查资源时带上tenant_id = 1,一行都查不到——所有@ss.hasPermission对非超管用户恒为 false,且没有任何报错。
写查询时别看到列就以为它生效。逐表核对见权限资源。
设计上的分工是:sys_resource 回答「这套系统里存在哪些可授权对象」(与租户无关,system_menu 本来就是 @TenantIgnore 的),租户差异体现在 sys_grant(谁被授予了什么)。
接口清单
9 个 Controller(controller/admin/permission/)。完整逐端点清单见各子页,这里只给全局视图与刻意不挂权限点的端点:
| Controller | 端点数 | @PreAuthorize 数 | 说明 |
|---|---|---|---|
ResourceAdminController | 8 | 8 | 资源 CRUD、树、有效树 |
GrantAdminController | 10 | 9 | 授权 CRUD、批量、有效树、my-field-modes |
RuleAdminController | 9 | 9 | 行级规则 CRUD、模板元数据 |
SlotMappingAdminController | 7 | 7 | 槽位映射 CRUD |
SubjectAdminController | 1 | 1 | 用户组 |
RecordShareController | 4 | 0 | 🔴 全部刻意不挂 |
PermissionController | 5 | 5 | 角色↔菜单(旧路径) |
PermissionDiagController | 1 | 1 | 诊断 |
DataScopeMigrateController | 2 | 2 | @DataScope 迁移 |
RecordShareController 为什么一个权限点都不挂
:34 的注释:分享是每个人对自己单据的日常动作,挂权限点要么给所有人授(等于没挂)要么功能作废。真正的边界在 RecordShareService#checkShareable,它返回 6 种具体拒绝原因,由 Controller 用 exception0 带出去(SHARE_FORBIDDEN_CODE = 1_002_031_000)——拒绝原因有五六种,合并成一句「无权分享」用户不知道该改什么。
my-field-modes 为什么不挂
GrantAdminController:102-104 的 @Operation 逐字写着:「不挂权限点——它返回的是调用者自己的权限,挂了反而会让没有授权管理权限的普通用户拿不到自己的字段权限」。
GrantAdminController 的读路径规则
类注释禁止把查询搬进 GrantAdminService(那会造出第三份鉴权口径),所以读路径直接打 Mapper。唯一例外是 /effective-tree(:84-93):它要的是「引擎判出来是什么」而不是「台账上写了什么」,委托给 GrantDecisionService 恰恰是复用那份权威口径。在 Controller 里自己拼一遍判定才是真正的第三份。
怎么扩展
三条最常见的扩展,各有独立子页:
| 我要… | 去哪 | 最容易漏的一步 |
|---|---|---|
| 加一种授权主体类型 | 授权与决策引擎 | 🔴 9 步里顺序不可逆:先加枚举再改闭包,反过来会让存量行算不出来 |
| 加一条行级规则模板 | 行过滤规则 | 🔴 expandBuiltInTemplate ↔ RuleTemplateMetaRegistry.paramSpecs 是手工对齐的契约;且表必须先在 tableNames 集合里 |
| 加一个动作 | 授权与决策引擎 | 🔴 VERB_TO_ACTION 漏一项会让权限串排序错乱(实测 39 行错分,档 C 从 35 涨到 153) |
| 加一种资源类型 | 权限资源 | default_effect 出厂值要想清楚:它决定「零配置时放行还是拒绝」 |
| 改前端授权页 | 前端授权页开发 | GRANT_ACTION_OPTIONS 与后端枚举没有任何自动对齐 |
| 排查「配了不生效」 | 诊断与排错 | — |
坑与注意
跨子页的、属于体系层面的坑:
- 🔴
sys_resource是全局表,但列里有tenant_id。见上。 - 🔴 超管短路在引擎之外。
PermissionServiceImpl:96直接return true,所以超管账号测不出任何权限缺陷。验证权限改动必须用非超管账号。 - 🔴
TENANT_ADMIN没有短路。只有super_admin有(RoleServiceImpl.java:240)。租户管理员走的是正常判定路径。 - 两套
isSuperAdmin实现,刻意不同。DataRowFilterResolver.isSuperAdmin:341与PermissionServiceImpl那个不是同一份代码,别「统一」它们。 @DataPermission(enable = false)/executeIgnore不是可选的。权限体系自己的查询(读sys_grant、算闭包)必须绕过数据权限拦截器,否则求值过程会触发新的求值,无限递归或直接查不到数据。@Cacheable必须配readOnly=true+executeIgnore,理由同上。resourceChainCache没有 TTL。资源树改了要显式失效,否则判定读的是旧链。- 有效权限树刻意不加
@Cacheable,有 5 个显式失效点。 - 🔴 1900 个元素的 IN 列表实测耗时 36 秒。批量查授权必须用
selectListActiveBySubjects,不要拼大 IN。 FieldModeEnum没有VISIBLE取值。解析不出来的行被静默忽略并回落到「可编辑」——一条脏数据的方向是放开而不是收紧。- 过期时间对 ALLOW 和 DENY 一视同仁(刻意)。且 SQL 与内存两处比较一个用
>一个用>=,边界上差一秒。 sys_record_share只放开不收紧。取消共享 ≠ 禁止查看:被取消的人若通过别的途径本来就看得到,照样看得到。要真挡住得用sys_grant的 DENY。
→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引。
测试
| 测试 | 守什么 |
|---|---|
GrantDecisionEngineTest | 八步判定、优先级、两个早退、回落 |
GrantDecisionServiceTest:224 | 塌缩翻译、缓存、有效树 |
CollapseMigrationSelfCheckTest | 🔴 拿计划器重放迁移前冻结快照 _scratch/s2c_snapshot.json,5/5 |
ResourceCollapsePlannerTest / ResourceCollapseApplierTest | 塌缩计划与应用 |
PermissionStringResolverTest | 6 级 Tier 解析 |
DatasetResourceServiceImplTest | 22 个 @Test,登记五步 |
RecordShareServiceTest / DataRowFilterRecordShareTest | 共享的 6 种拒绝原因 |
全部纯 Mockito,无 DB 集成测试。前端侧 src/views/system/{role/grant,resource} 下另有 6 个 spec,见前端授权页开发。
CollapseMigrationSelfCheckTest 换判据就会失去意义
它校验的是「计划器对冻结快照重放的结果 == 迁移真实产出的世界」。改了塌缩判据之后,它校验的变成「一份从未被执行过的计划」,而迁移不可重跑。这是「换判据」这条根治路线被否掉的理由之一。
未证实
- 本页的判定链逐段读了源码,但没有端到端跑一遍「配一条授权 → 用非超管账号验证」来确认三层叠加的实际行为。3 张被打穿的表单是历史实测(记在
application.yaml与GrantDecisionService的注释里),不是本轮复现。 - 两个 MyBatis 拦截器(数据权限、租户)的相对顺序没有代码钉住,只从行为推断。
RuleTemplateEnum的 javadoc 第 14 行写「8 个内置模板」,实际声明了 10 个——注释过期,未确认哪个是权威。- 所有「实测 N」(1549/1550、18/9、341/666、1022→598、1802→1787、36 秒…)都来自源码注释,是历史快照。
GrantDecisionService:482-484专门警告「别把任何一个数字当常量用」。 _scratch/s2c_snapshot.json未读。- 约 11 个类只读到签名层(
SubjectClosureServiceImpl、PermissionTreeDeriver、ConditionTreeEvaluator的部分方法等)。
相关页面
本域子页
- 权限资源 —
sys_resource的七种类型、树、default_effect、菜单同步 - 行过滤规则 — 10 个内置模板、条件树、槽位、拦截器
- 授权与决策引擎 — 八步判定、塌缩、回落、主体闭包
- 前端授权页开发 —
ResourceTreePicker/GrantEffectiveTree/ 批量授权 - 诊断与排错 — 「配了不生效」的排查路径
跨域
