字段权限
字段权限是列级权限:用来控制"同一行数据里,某个字段列能不能看原文"。与数据权限(行级权限)正交、与功能权限(管接口/菜单/按钮)互补。典型场景:用户管理列表中,普通角色看到的手机号是 138****5678(脱敏),甚至整列不可见(隐藏)。
1. 核心模型
拒绝模型:字段权限是管"谁被限制看什么" ,未配置字段权限视为拥有该字段的全部权限。两种处理动作:
| 动作 | ruleType | 效果 | 说明 |
|---|---|---|---|
| 隐藏 | 10 | 字段值置 null(不是从 JSON 删 key,结构保持稳定) | 前端再按规则隐藏整列 |
| 脱敏 | 20 | 字段值变形(如 138****5678),仅对非空 String 生效 | 前端直接展示变形值即可 |
同名字段统一受限是特性:响应对象树中所有同名 property 统一处理——"同一页面里'手机号'就该统一脱敏",配置时无需关心 VO 类的层级。
注意:字段权限是决定该角色看不到某字段的值;应用、功能、数据权限则是决定该角色能看什么值。
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 | 张*丰(1字原样 / 2字留首 / 3字留首尾 / 4字留后2 / >4字留前2后1) |
email | zha**@x.com(保留域名;本地部分 1~2 字全掩、3~4 字留前2、≥5 字留前3) |
id_card_number | 留前3后4(≥15 位才脱敏) |
bank_card_number | 留前4后4(≥8 位才脱敏) |
fixed_phone | 座机留前3后2 |
car_license | 车牌留前3后1 |
address | 地址留前6掩3(4~6 字留前3掩3、≤3 字原样) |
password | 全掩码长度不变 |
自定义脱敏规则(二开入口):MaskManager.registerMaskProcessor(...)或 BuiltinMasker.register(name, rule)。注册后运行时(含后端保存校验)即可识别——运行时优先走 MaskManager,未注册才回退 BuiltinMasker。但注意:配置页的脱敏规则下拉是前端硬编码的 9 项(web-console/src/api/console/permission/resourceField/model.ts 的 MaskRuleOptions),自定义规则需同步在该常量中追加选项,才能在配置页下拉中选择。
总开关:mdp.ignore.field-auth-enabled(默认 true,灰度期可整体关闭)。
3. 授权(角色管理 · 字段权限 Tab)

角色管理 → 选中角色 →「字段权限」Tab:
- 展示方式为应用 → 菜单 → 字段的勾选树:菜单节点不可勾选,字段叶子带"隐藏"(红)/"脱敏"(橙) Tag;
- 整棵树由聚合接口
GET /permission/role/fieldAuthTree一次返回:后端按已授权应用分组装配,可勾选范围 = 同性质权限集合角色可授权的资源树(与功能权限 Tab 同一上限,裁掉无字段规则的分支后把字段规则作为叶子挂到对应菜单下); - 保存
POST /permission/role/saveRoleField(入参roleId + fieldIdList,全量覆盖:提交的集合 = 该角色被限制查看的字段,空集合=清空);保存时强校验:普通角色提交的字段不得超出同性质权限集合角色的字段池(模板角色豁免),超出直接报错; - 回显随
fieldAuthTree一并返回(内部调findFieldIdsByRoleId装配checkedFieldIds);保存后失效该角色下所有用户的缓存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(文件下载/导出)时不做字段处理。注意:字段权限仅在响应层(JSON 序列化前)生效,不影响 DB 读取;
缓存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 | 规则加载完成标志(避免加载前误渲染应隐藏的列) |
rules | 当前页面受限规则原始 Map(prop → ruleType) |
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(与接口放行集缓存一并失效) |
| 菜单删除(含子孙菜单) | 级联删除该菜单的字段规则与角色字段授权(ResourceFieldServiceImpl.deleteByMenuIds),随后删缓存A + 按 fieldId→角色→用户反查删缓存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;