字段权限
字段权限是列级权限:控制"同一行数据里,某个字段能不能看原文"。与数据权限(管行)正交、与功能权限(管接口/菜单)互补。典型场景:用户管理列表中,普通角色看到的手机号是 138****5678(脱敏),甚至整列不可见(隐藏)。
1. 核心模型
拒绝模型:字段权限是管"谁被限制看什么" ,未配置字段权限视为拥有该字段的全部权限。两种处理动作:
| 动作 | ruleType | 效果 | 说明 |
|---|---|---|---|
| 隐藏 | 10 | 字段值置 null(不是从 JSON 删 key,结构保持稳定) | 前端再按规则隐藏整列 |
| 脱敏 | 20 | 字段值变形(如 138****5678),仅对非空 String 生效 | 前端直接展示变形值即可 |
同名字段统一受限是特性:响应对象树中所有同名 property 统一处理——"同一页面里'手机号'就该统一脱敏",配置时无需关心 VO 层级(FieldPermEngine 类注释)。
注意:字段权限是决定该角色看不到某字段的值;应用、功能、数据权限则是决定该角色能看什么值。
2. 配置
字段规则挂在菜单上(mdc_resource_field 表),配置入口:菜单管理 → 选中菜单/按钮 → 右侧「字段配置」Tab,手工录入(非扫描):
| 字段 | 说明 |
|---|---|
menuId | 所属菜单(保存时必填) |
property | VO 的 Java 属性名(如 phone),格式 ^[a-zA-Z][a-zA-Z0-9_]*$,同菜单下唯一 |
name | 展示名(如"手机号") |
ruleType | 处理动作:10-隐藏 / 20-脱敏 |
maskRule | 脱敏规则名(ruleType=20 时必填,须已注册) |
state | 启用状态 |
后端只校验格式与脱敏规则已注册,不校验属性真实存在——引擎运行时找不到属性即跳过,因此 VO 字段改名后记得同步配置。
内置脱敏规则(BuiltinMasker,注册名与 mybatis-flex Masks 生态一致):
| 规则名 | 效果示例 |
|---|---|
mobile | 138****5678(1 开头 11 位,留前3后4) |
chinese_name | 张*丰(2字留1 / 3字留首尾 / 更长留前2后1) |
email | zha**@x.com(保留域名,本地部分留 1~3 位) |
id_card_number | 留前3后4(≥15 位才脱敏) |
bank_card_number | 留前4后4(≥8 位才脱敏) |
fixed_phone / car_license / address / password | 座机留前3后2 / 车牌留前3后1 / 地址留前6掩3 / 全掩码长度不变 |
自定义脱敏规则(二开入口):MaskManager.registerMaskProcessor(...)或 BuiltinMasker.register(name, rule)——注册后配置页下拉与运行时同时识别。运行时优先走 MaskManager,未注册才回退 BuiltinMasker。
总开关:mdp.ignore.field-auth-enabled(默认 true,灰度期可整体关闭)。
配置接口(前缀 /permission/resourceField):POST /save、/update、/delete、/page、/list、GET /getById、GET /current?menuId=(查当前用户在该菜单下的受限规则,前端渲染用)。
3. 授权(角色管理 · 字段权限 Tab)
角色管理 → 选中角色 →「字段权限」Tab:
- 形态为应用 → 菜单 → 字段的勾选树:菜单节点不可勾选,字段叶子带"隐藏"(红)/"脱敏"(橙) Tag;
- 可勾选范围 = 该角色功能权限范围内的菜单树(逐应用调
treeByRoleId装配,裁掉无字段规则的分支); - 保存
POST /permission/role/saveRoleField(入参roleId + fieldIdList,全量覆盖:提交的集合 = 该角色被限制查看的字段,空集合=清空); - 回显用
GET /permission/role/findFieldIdsByRoleId;保存后失效该角色下所有用户的缓存B。
4. 鉴权(后端):FieldPermAdvice + FieldPermEngine
FieldPermAdvice(md-mvc-flex,@ControllerAdvice + ResponseBodyAdvice)在 JSON 序列化写出前原地改写响应体。决策链(任一环节不命中即放行):
① 总开关 mdp.ignore.field-auth-enabled
② 当前用户已登录(定时任务/worker 无 HTTP 上下文,返回 null 放行)
③ URI 归一化(复用 ApiPermChecker.normalizePath,剥网关前缀 api + 服务前缀)
④ 缓存A 反查:该 URI 是否对应"配置了启用字段规则的菜单"
—— 未命中即放行,绝大多数请求在此零开销短路
⑤ 缓存B 取当前用户受限集(运营管理员豁免),命中则 FieldPermEngine.apply(body, rules)放行通道:响应体为 byte[]/Resource(文件下载/导出)直接返回;导出场景的字段权限由导出基类手动调引擎(当前为预留)。
缓存A(URI→菜单映射,key resource_field_uri_menu:id):全量预解析 mdc_resource_api + mdc_resource_menu + 启用字段规则菜单——接口绑定的资源(菜单或按钮)沿上级链找最近一个配置了启用字段规则的菜单,map key 形如 "GET /system/user/page"。
缓存B(用户受限集,key user_field_perm:id:{userId}):用户启用角色 → 任一角色为 OPERATIONS_ADMIN 直接豁免(不再查字段表);否则按 menuId → property → FieldRule 聚合(多角色同名字段后写覆盖先写,无优先级设计)。
FieldPermEngine 遍历逻辑(纯函数,无 web/DB 依赖):
- 容器下钻不占深度:
Collection/Map/数组/R.data/mybatis-flexPage均可穿透到业务数据; - 保护阈值:最大下钻 5 层 bean、单次最多 5000 对象(超限告警截断)、
IdentityHashMap防循环引用; - 命中处理:隐藏=置 null(JavaBeans setter 优先,链式 setter 回退字段直写);脱敏=仅处理非空 String(非字符串字段配脱敏不生效);
- 单属性读/写失败只记日志,不拖垮整个响应。
5. 鉴权(前端):useFieldPerm()
web-console/src/utils/fieldPerm.ts 组合式函数:按当前路由 path 反查 menuId → 调 GET /permission/resourceField/current 拉规则(模块级缓存,失败按无限制处理),返回:
| 能力 | 用途 |
|---|---|
isHidden(prop) / isMasked(prop) | 判定字段是否隐藏/脱敏 |
loaded | 规则加载完成标志(避免加载前误渲染应隐藏的列) |
stripRestricted(params, original) | 编辑页防回写(见下方警告) |
// 列表页:规则加载后,隐藏字段整列移除(含搜索项)
watch(loaded, (isLoaded) => {
if (!isLoaded) return
for (const prop of Object.keys(rules.value)) {
if (!isHidden(prop)) continue
gridApi.grid?.hideColumn?.(prop) // 隐藏 vxe-table 列
gridApi.formApi?.removeSchemaByField?.([prop]) // 同步移除搜索项
}
}, { immediate: true })
// 脱敏字段无需处理——服务端已返回变形值
// 编辑表单:受限字段跳过唯一性校验(回显值非真实值),提交前剔除
data = stripRestricted(data, state.formData)接入字段权限的编辑页必须防回写
编辑回显数据已被服务端处理(隐藏=null、脱敏=变形值),直接提交会用 null/变形值覆盖真实数据。stripRestricted 的规则:隐藏字段无条件剔除;脱敏字段值与回显原值一致(用户未改)时剔除、输入新值则保留提交。
6. 缓存与失效
两级缓存均 24h TTL,失效触发点全部在 console 侧写操作:
| 变更场景 | 失效动作 |
|---|---|
| 字段规则增/改/删 | 删缓存A(全量重建)+ 按 fieldId→角色→用户反查删缓存B |
| 角色字段授权变更(saveRoleField) | 该角色下所有用户的缓存B |
| 用户-角色绑定/解绑、角色资源变更、角色更新/停用/删除 | 该角色(或用户)的缓存B(与接口放行集缓存一并失效) |
7. 示例:「用户管理」手机号的隐藏与脱敏
配置:菜单管理 →「用户管理」→ 字段配置 Tab,录入两条规则:
| property | name | ruleType | maskRule |
|---|---|---|---|
phone | 手机号 | 20-脱敏 | mobile |
email | 邮箱 | 10-隐藏 | — |
授权:角色管理页 →"人事专员"→ 字段权限 Tab → 勾选「用户管理 → phone(脱敏)」与「用户管理 → email(隐藏)」。
鉴权效果:
- 该角色用户调
POST /organization/user/page→ 缓存A 命中「用户管理」菜单 → 缓存B 命中受限规则 → 响应中每行phone变为138****5678、email变为null; - 列表页
useFieldPerm()加载规则后自动隐藏 email 整列(含搜索项);phone 列直接展示脱敏值; - 编辑用户时:email 字段不渲染;phone 显示"已脱敏,输入新值以修改",提交前
stripRestricted剔除未改动的脱敏值,避免覆盖真实手机号; - 运营管理员访问同一接口 → 缓存B 豁免,看到全部原文。
8. 二次开发要点与排障
- 新页面接入:菜单「字段配置」录入规则 → 角色「字段权限」授权 → 前端列表页
useFieldPerm()隐藏列、编辑页必须stripRestricted; - VO 字段改名需同步配置:引擎对找不到的属性静默跳过,配置不会报错但会失效;
- 排障速查:字段没脱敏 → ①菜单字段规则是否启用 ②角色是否授权受限 ③接口是否关联到该菜单/按钮(缓存A 能否命中)④当前用户是否运营管理员(豁免)⑤
mdp.ignore.field-auth-enabled开关 ⑥编辑页数据被覆盖 → 检查是否漏了stripRestricted。