权限与脱敏(前端)
前端在页面 / 字段 / 操作三个层级按权限码过滤,并在服务端脱敏的基础上做只读与标识提示。本页讲前端这一侧怎么写;判定规则本身(权限码、数据范围、ABAC、约束规则)见 权限模型。
前提:前端过滤是体验,不是安全边界
服务端才是安全边界
前端隐藏按钮只是不让用户看到无效入口。真正的门控在服务端:[PermissionAuthorize] 特性 + 授权快照判定。
绕过前端直接调接口一定会被后端拒绝——所以不要因为前端已经隐藏了按钮就省掉后端的权限标注。
权限码
以 : 分段,多数是 module:resource:action(如 saas:user:read、saas:tenant-edition-permission:grant),也有两段的 module:action(如 workflow:execute)。超管在服务端用字面通配 *(不是段级 *:*:*)。
前端不把 * 当通配,只按下发的权限码与按钮码精确匹配:服务端下发时已把超管的 * 展开成当前上下文生效的全部码,作用侧不含当前上下文的码(如平台态里的部门、数据范围等租户侧权限)不下发、鉴权时 * 也不放行。前端再拿 * 短路,就会在平台态露出服务端必拒的操作。
真源在后端 SaasPermissionDefinitions(Saas 模块)与各模块自己的权限码类。前端只是消费者——写权限码字符串时去后端对一下,别凭记忆写。
用户的权限码集合来自登录后的 GET /api/Auth/Permissions。
权限码不在 JWT 里
Access Token 里没有具体权限码(超管例外,只放一个 * 作快路径)。这带来一个好性质:管理员改了授权,用户不用重新登录就生效——前端下次拉取权限或后端下次判定时就是新的。
三级过滤
一、页面级
由路由守卫按 meta.roles / meta.permissions 校验,无权跳 403。数据来自后端 PageDescriptor.PermissionCode。
PermissionCode 为空的页面是纯展示菜单,不做门控。
二、字段级
Schema 页里字段声明 permission,无权时该列与该搜索项整个不渲染(selectors.ts 的 isFieldPermitted):
{ key: 'phone', title: t('identity.org.col_phone'), dataType: 'string',
permission: 'saas:department:read' }注意这是「不渲染」而不是「渲染成空」——用户看不到这一列存在。
三、操作级
- Schema 页的
actions[].permission:无权时按钮不出现。 PageSchema上的exportPermission/importPermission/removePermission(批量删除)/statusPermission(批量启停):门控对应的内置按钮。其中只有exportPermission是严格门控(未声明就不显示导出),另外三个未声明时不做限制。
exportPermission 未声明 = 不显示导出
这是刻意的默认值选择:宁可漏显示,不可错显示。要导出按钮就必须声明权限码。
页面里手写门控
没有包裹式权限组件——页面按钮/区块级门控统一是「usePermission() 派生 computed + 模板 v-if」这一种写法:
const { hasPermission } = usePermission()
const canGrant = computed(() => hasPermission('saas:tenant-edition-permission:grant'))<XhButton v-if="canGrant">分配权限</XhButton>usePermission()(~/hooks/usePermission.ts)提供:
| 方法 | 说明 |
|---|---|
hasPermission(code | code[]) | 是否有权限码(传数组时任一命中即通过) |
hasRole(role | role[]) | 是否有角色(传数组时任一命中即通过) |
hasAnyPermission(codes[]) | 任一命中即通过 |
统一用这一种写法
不要另造包裹组件或指令。全仓统一「computed + v-if」的好处是:审计权限点时一个 grep 就能找全,也不会出现「指令生效时机与渲染时机不一致」的坑。
字段级脱敏(FLS)
脱敏在服务端完成
后端返回的敏感字段已经是打码后的值(如 138****5678),前端不再二次打码,页面也不需要为字段安全写任何代码:
- 读:列表、详情、导出拿到的就是打码后的值;
- 写:表单把拿到的脱敏值或空值原样交回,后端视为没改并还原原值——不会用
138****5678覆盖真实号码;改动了只读字段,后端返回「字段「…」当前用户无修改权限」。
规则在「字段安全」页按实体、字段下拉配置,模型见 数据权限 · 字段级安全。
字段安全也门控查询条件
服务端会剔除读受保护字段上的排序、过滤与关键字搜索字段,剔完没有有效排序时回退默认排序。
所以「排序点了没反应」「按某字段搜索没效果」优先怀疑字段安全,而不是前端 bug。
多租户
数据按 TenantId 隔离,全局数据约定 TenantId=0。前端不需要手动处理租户过滤——它由后端在请求管道里完成,前端只按拿到的权限码集合渲染。
前端要处理的只有两件事:
| 事情 | 说明 |
|---|---|
| 切换上下文 | 走 authStore.switchContext(tenantId)(null 回平台,仅平台账号):服务端在目标上下文续接会话、签发新令牌、旧令牌失效;前端清掉用户信息与标签页后整页重建,不产生新登录/新设备 |
| 上下文判断 | UserInfo.isPlatform(当前是否在平台)、tenantName(当前租户名)、canAccessPlatform(是否平台账号);页头的上下文标识据此显示并进入控制中心 |
整页加载时路由守卫把用户信息与权限、菜单一起重取——租户不同,可用的功能也不同(受租户版本门控影响),本地缓存的用户信息不作为上下文依据。
排查
| 现象 | 原因 |
|---|---|
| 按钮不显示 | 权限码写错(去后端 SaasPermissionDefinitions 对);或用户确实没这个权限 |
| 导出按钮不显示 | PageSchema.exportPermission 没声明 |
| 列不显示 | 字段 permission 无权;或 visible: false |
| 保存时报「无修改权限」 | 字段安全规则让该字段对当前用户只读,而表单改了它的值 |
| 前端能点、后端 403 | 正常——前端过滤是体验层,以后端为准。检查该接口的 [PermissionAuthorize] 是否与前端用的码一致 |
| 改了授权前端还是旧的 | 重新拉 /api/Auth/Permissions;后端侧检查是否调了 InvalidateAuthorizationAsync |
相关页面
- 权限模型:权限码、RBAC 继承、数据范围、ABAC、约束规则
- Schema 驱动页面:字段与操作的权限声明位置
- 路由与菜单:页面级门控
- 多租户与版本:版本门控与租户切换
